Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
41fb2a0
feat: lazy authentication in mcp server
Aug 6, 2026
e363570
fix: second attempt for optional login
Aug 7, 2026
668cee9
fix: third attempt for optional auth - with query parameter
Aug 7, 2026
9ba4b06
fix: third attempt for optional auth - with query parameter
Aug 7, 2026
e78ff01
fix: mcp oauth endpoint urls
Aug 7, 2026
e7370a9
fix: mcp oauth endpoint urls
Aug 7, 2026
1bb7916
fix: mcp oauth endpoint urls
Aug 7, 2026
bcc59b3
fix: mcp oauth endpoint urls
Aug 7, 2026
7ec6c34
fix: another attempt for mcp client login
Aug 10, 2026
840829d
feat: token exchange in mcp server
Aug 11, 2026
161b545
fix: token exchange endpoint config
Aug 11, 2026
92e5cb2
fix: debug failed token exchange
Aug 11, 2026
1debefb
fix: token exchange config bug
Aug 11, 2026
87ae2cc
fix: token for internal mcp
Aug 12, 2026
51139ae
fix: usertoken instead of converationid from chatbot to mcp
Aug 12, 2026
c1c44bf
fix: usertoken instead of converationid from chatbot to mcp
Aug 12, 2026
a17bab2
fix: mcp_url regex for internal parameter
Aug 13, 2026
4b21271
fix: mcp anonymous dcr flow
Aug 13, 2026
067a7ce
fix: mcp register proxy to kc
Aug 13, 2026
24a9888
fix: mcp register proxy to kc
Aug 13, 2026
0f45d6f
fix: mcp url remove internal query flag
Aug 13, 2026
2e233d1
fix: register endoint method
Aug 13, 2026
65f199c
fix: kc registration proxies and manifests for mcp
Aug 13, 2026
ca3c336
fix: kc registration proxies and manifests for mcp
Aug 13, 2026
65a16cf
fix: remove client request scope in register endpoint
Aug 13, 2026
bfe3b16
fix: mcp use an authenticated kc client
Aug 13, 2026
0b78261
fix: add env for mcp allowed hosts
Aug 13, 2026
8f11cee
fix: typescript laments
Aug 13, 2026
03bbaad
fix: attempt to fix perplxity trouble with mcp client registration
Aug 13, 2026
07d43eb
fix: cors for mcp
Aug 13, 2026
c316538
fix: cors
Aug 13, 2026
f6aa003
fix: generalize cors for mcp
Aug 13, 2026
5eeefd1
fix: debug logs for mcp authorize endpoint
Aug 14, 2026
60afb99
fix: appMcpAllowedHosts is a public runtime var
Aug 14, 2026
66a5081
fix: debug token endpoint for perplexity requests
Aug 14, 2026
9248497
fix: debug token endpoint for perplexity requests
Aug 14, 2026
a9f6a94
fix: force public client in register endpoint
Aug 14, 2026
c693885
fix: more debugging for mcp auth
Aug 14, 2026
252b459
fix: redirect_uri gating in mcp register
Aug 14, 2026
d19e9d5
fix: adapt register response
Aug 14, 2026
c6de7ef
feat: add chatgt and mistral support for mcp
Aug 14, 2026
771814b
feat: tag mcp created keycloak clients for management
Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ PLAYWRIGHT_TEST_PASSWORD=
# ==== MCP Settings ====
# NUXT_APP_MCP_SERVER_URL=https://localhost:8080/mcp/
# NUXT_APP_MCP_SERVER_TRACE=1
# NUXT_APP_MCP_KEYCLOAK_CLIENT_ID=mcp-registration-service
# NUXT_APP_MCP_KEYCLOAK_CLIENT_SECRET=XXXXXXXXXXX
# NUXT_PUBLIC_APP_MCP_ALLOWED_HOSTS='claude.ai|claude.com|localhost|127.0.0.1|perplexity.ai|www.perplexity.ai|enterprise.perplexity.ai'


# ==== SORBOBOT (optional) Settings ====
# NUXT_APP_SORBOBOT_API_TOKEN=YOUR_API_TOKEN_HERE
Expand Down
6 changes: 4 additions & 2 deletions k8s/projects-frontend/secrets.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ config:
NUXT_APP_CHATBOT_PROMPT_DB: ENC[AES256_GCM,data:d8f0AH3px8XODQlDseqNWCPb/B/xVkWaPqKP/8/DXfCluYXuFqNH1vGi200O9/HxPYRKXScwPjEDQFOzlROPWIrpSRaFo+lYPqxHmx3N2fa0UtFdAisz+xKrzHZrKfnxEgo53uARkeKB0rU0OuU4TBYK8GFk0clesPhAXQ+0RjelldU0V8ylf0pe3Jn1KrBuLo13KrzyS0/na9w=,iv:1gyZ7lRtMksaZqpxVKxSJxY+Ay7F/22XI+qKvbTakik=,tag:tipbXLd7oLT/1VO9ltqNmA==,type:str]
LANGSMITH_ENDPOINT: ENC[AES256_GCM,data:gfghYyHd7cSoMH49Htgf+8QsQyMcrxRjFUujRt9q6xuGXg==,iv:ufLAHX19mT59zljjYOs2CzjjxPWHQS5xJw1jJqi3hQY=,tag:PgByiy2V0DT/canWw4rcoA==,type:str]
LANGSMITH_API_KEY: ENC[AES256_GCM,data:qooj4kGWd4hDlBcRVkGfPT9T0t5jJMbXOjcNtnIIfYXW7Kiv4W9BiCYoqrzPazOY9dWF,iv:K5h60KXlcNCH9JdeEy9CAqmOPumhLn6idC8dS/4bQTU=,tag:TfKVvLpdJYUhSoFW1iWQ3A==,type:str]
NUXT_APP_MCP_KEYCLOAK_CLIENT_ID: ENC[AES256_GCM,data:F7XySKsufW7hLdSQwrXgfez3u5CRjiEl,iv:fsq7NjUUi/NNOApgsUJu5B5xm7prFGIg1o9YC5gkWD8=,tag:GvzwK1+2o85phVEbwhp01w==,type:str]
NUXT_APP_MCP_KEYCLOAK_CLIENT_SECRET: ENC[AES256_GCM,data:AQrQdFC1zxCk7/JsYdMQdlTTKKi1vozsOm9wChXTO31WDDAxNdofJYGLZpO562D1GaCXYTOF/UliIO9Pp1XHM0P9YoRNuf4hvLkJy27I/Y/qBQQUqbM=,iv:nLYuEnG/PXQygZolHMaoCWEw0BMmxHjF9KDj1IWzoko=,tag:d7shs68kCveWJuOdQnOy7Q==,type:str]
e2eEnv:
sensitive:
USER_ADMIN_PASSWORD: ENC[AES256_GCM,data:RXWsMLtLB8v5YSWB,iv:tK9wwRpTuXdSFy5jyjk5aQvXAI58MZiu+aqVXHiz+3Q=,tag:LWyz8EF5wivXiw7BnWz+Fg==,type:str]
Expand Down Expand Up @@ -87,7 +89,7 @@ sops:
RzNiMEZMK1FKUUkrZnBrQks0T24xMTQKmtRTd/JNinFVCUr8ZHAb5pxvuXKe4fZc
6G8q+m7BuTcuIKcYc0YatdR9Jsd31J7koxXfCh8LJfdATK09NpiEHA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-06-03T13:06:46Z"
mac: ENC[AES256_GCM,data:5iE3PFjSzxpoFEM1aj7ksMTpYCLJ7cFkCwMCDHIZRYn1OYXfD+k3ZzZECx5c5kRGFPJDKo4byPr9+/PdM/cW171OOg7bF/mil/dByywQd1iq2BnG5XcN4WijO1yOE2Zwf9DFK1B+kzbLV920ubOBWeUpePnjjDGT+HR5obfz7us=,iv:hkMbjrRAnMsvaWW1EvDTwJVngOKCtgYT/8fwYeyfeeI=,tag:30LPsnO2Kq1wRXkPYyyL+A==,type:str]
lastmodified: "2026-08-13T14:50:48Z"
mac: ENC[AES256_GCM,data:FofIzEu+JvOz+HNQRYOCrV5cKj/m9cA0fcafE2NNlzpC+FoYTJWqZjtcml2O5vOX61fM6KfGxnOc1g0OZ+PwufHmglCzl1a1dITPpdZzBzWvncM2VQ5nT6OZ20WahzF6s23mOqKP9CkDG6k0PMn2scFcg7NcJrOzQlmfZX71RWE=,iv:qD6LEZiAP4lB9ZE91MHVo+2rlcISVI5NDnw4+CG37sY=,tag:1cDmVWMunRcBOsop86SEQg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.2
1 change: 1 addition & 0 deletions k8s/projects-frontend/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ config:
NUXT_APP_LANGCHAIN_TRACE: ''
NUXT_APP_GOTENBERG_SERVER_URL: ''
NUXT_PUBLIC_APP_GOTENBERG_ENABLED: ''
NUXT_PUBLIC_APP_MCP_ALLOWED_HOSTS: 'localhost|127.0.0.1|claude.ai|claude.com|perplexity.ai|www.perplexity.ai|enterprise.perplexity.ai|chatgpt.com|callback.mistral.ai'
e2eEnv:
nonSensitive:
USER_ADMIN_EMAIL: testautomatatiquedministrateur1@outlook.fr
Expand Down
3 changes: 3 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ export default defineNuxtConfig({
appOpenaiApiPromptVersion: '',
appOpenaiApiVectorStoreId: '',
appMcpServerUrl: '',
appMcpKeycloakClientId: '',
appMcpKeycloakClientSecret: '',
appSorbobotApiToken: '',
appMcpServerTrace: 0,
appLangchainTrace: 0,
Expand All @@ -214,6 +216,7 @@ export default defineNuxtConfig({
appApiOrgCode: '',
appFavicon: '',
appCaptchaKey: '',
appMcpAllowedHosts: '',
appMixpanelApiUrl: '',
appMixpanelProjectToken: '',
appPublicBinariesPrefix: '',
Expand Down
4 changes: 3 additions & 1 deletion src/mcp-server/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'

import registerOrganizationTool from './projects/organization-tool'
import registerInstructionTool from './projects/instruction-tool'
import registerSorbobotTool from './sorbobot/sorbobot-tool'
import registerProjectTool from './projects/project-tool'
import registerSearchTool from './projects/search-tool'
import registerPeopleTool from './projects/people-tool'
// import registerLoginTool from './projects/login-tool'
import registerEventTool from './projects/event-tool'
import type { TypeMcpServer } from '~/interfaces/mcp'
import registerNewsTool from './projects/news-tool'
Expand All @@ -31,6 +31,8 @@ function createMCPServer() {
// !!sorbobotApiToken
// )

// registerLoginTool(server)

registerSorbobotTool(server)

registerSearchTool(server)
Expand Down
15 changes: 6 additions & 9 deletions src/mcp-server/projects/base.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import type { ClientAPIOptions } from 'shared-projects-frontend/apis'
import { traceMcp } from '@/server/projects-agent/tracers/trace-mcp'
import type { InferSchema, ToolCallback } from '~/interfaces/mcp'
import { tokenMap } from '~/server/routes/api/chat-stream'

const runtimeConfig = useRuntimeConfig()
export const orgCode = runtimeConfig.public.appApiOrgCode

export function getUserToken(extras) {
const convesrationId = (extras.requestInfo.headers['authorization'] || '').replace('Bearer ', '')
traceMcp('Tool Getting user token for conversationId', convesrationId)
const tokenEntry = tokenMap.get(convesrationId)
if (tokenEntry) {
traceMcp('MCP tool found token for conversationId', tokenEntry.token.substring(0, 6) + '...')
return tokenEntry.token
} else {
traceMcp('MCP tool no token found for conversationId', convesrationId)
console.log(extras)
const token = (extras.requestInfo.headers['authorization'] || '').replace('Bearer ', '')
if (token) {
traceMcp('MCP tool user token has token ', token.substring(0, 6) + '...')
return token
}
traceMcp('MCP tool user has no token')
return null
}

Expand Down
30 changes: 30 additions & 0 deletions src/mcp-server/projects/login-tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { TypeMcpServer } from '~/interfaces/mcp'
import { getUserToken } from './base'

export default (server: TypeMcpServer) => {
// Add an search tool
server.registerTool(
'login',
{
title: 'Login',
description:
'Allow the user to login to unlock extra data. If this tool is present, always first propose the user to use it.',
},
(extras) => {
if (!getUserToken(extras))
return {
content: [
{
type: 'text',
text: 'Use the PRM document (/.well-known/oauth-protected-resource) to authenticate the user.',
},
],
}
return {
content: [
{ type: 'text', text: 'User is authenticated, do not use the login tool again.' },
],
}
}
)
}
4 changes: 2 additions & 2 deletions src/server/projects-agent/agent/get-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import getVectorStore from '@/server/utils/vector-db-new.js'
import { SystemMessage } from '@langchain/core/messages'
import { createAgent } from 'langchain'

export default async function getAgent(agentData, event, conversationId) {
export default async function getAgent(agentData, event, userToken) {
const tools = []

/* --------- MCPs --------- */

if (agentData.useProjectsMcp || agentData.mcps.length) {
const mcpTools = await getMcpTools(agentData, event, conversationId)
const mcpTools = await getMcpTools(agentData, event, userToken)
tools.unshift(...mcpTools)
}

Expand Down
4 changes: 2 additions & 2 deletions src/server/projects-agent/tools/get-mcp-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { appMcpServerUrl } = runtimeConfig
export default async function getMcpTools(
agentData: any,
event: any,
conversationId: string
userToken: string
): Promise<any[]> {
const mcpConfigs = {}

Expand All @@ -18,7 +18,7 @@ export default async function getMcpTools(
transport: 'http', // HTTP-based remote server
url: appMcpServerUrl,
headers: {
Authorization: `${conversationId}`,
Authorization: `${userToken}`,
},
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/server/projects-agent/tracers/trace-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ const { appMcpServerTrace } = runtimeConfig

export const traceMcp = (...args) => {
if (appMcpServerTrace) {
console.log('[MCP TRACE]', ...args)
console.log(new Date().toISOString(), '[MCP TRACE]', ...args)
}
}
23 changes: 23 additions & 0 deletions src/server/routes/.well-known/oauth-authorization-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { applyMcpCors } from '@/server/utils/mcp-cors'
export default defineLazyEventHandler(() => {
const { appMcpServerUrl } = useRuntimeConfig()

const MCP_URL = appMcpServerUrl.replace(/\?internal=true$/, '').replace(/\/mcp\/?$/, '') // this server's canonical URI
// const RESOURCE = `${MCP_URL}/mcp` // RFC 8707 resource indicator
// const ISSUER = `${appKeycloakUrl.replace(/\/?$/, '')}/realms/${appKeycloakRealm}/`

return defineEventHandler(async (event) => {
if (applyMcpCors(event)) return
return {
issuer: MCP_URL,
authorization_endpoint: `${MCP_URL}/authorize`,
token_endpoint: `${MCP_URL}/token`,
registration_endpoint: `${MCP_URL}/register`,
response_types_supported: ['code'],
grant_types_supported: ['authorization_code', 'refresh_token'],
code_challenge_methods_supported: ['S256'],
token_endpoint_auth_methods_supported: ['none', 'client_secret_basic'],
scopes_supported: ['openid', 'profile', 'email', 'offline_access', 'mcp:tools'],
}
})
})
17 changes: 17 additions & 0 deletions src/server/routes/.well-known/oauth-protected-resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { applyMcpCors } from '@/server/utils/mcp-cors'
export default defineLazyEventHandler(() => {
const { appMcpServerUrl } = useRuntimeConfig()
const MCP_URL = appMcpServerUrl.replace(/\?internal=true$/, '').replace(/\/mcp\/?$/, '') // this server's canonical URI
const RESOURCE = `${MCP_URL}/mcp` // RFC 8707 resource indicator
// const ISSUER = `${appKeycloakUrl.replace(/\/?$/, '')}/realms/${appKeycloakRealm}/`

return defineEventHandler(async (event) => {
if (applyMcpCors(event)) return
return {
resource: RESOURCE,
authorization_servers: [MCP_URL],
bearer_methods_supported: ['header'],
scopes_supported: ['mcp:tools'],
}
})
})
22 changes: 1 addition & 21 deletions src/server/routes/api/chat-lg-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { traceMcp } from '@/server/projects-agent/tracers/trace-mcp'
import type { BaseMessageChunk } from '@langchain/core/messages'
import { initChatModel } from 'langchain/chat_models/universal'
import { MultiServerMCPClient } from '@langchain/mcp-adapters'
import { tokenMap } from '~/server/routes/api/chat-stream'
import getVectorStore from '~/server/utils/vector-db-new'
import { createAgent, createMiddleware } from 'langchain'
import { MemorySaver } from '@langchain/langgraph'
Expand All @@ -22,10 +21,6 @@ const {
} = runtimeConfig
const { appChatbotEnabled } = runtimeConfig.public

// TODO use own token map instead chat-stream one when refactoed
// Map conversationId to token and date for authed api requests in MCP
// export const tokenMap = new Map<string, { date: Date; token: string }>()

export const checkpointer = new MemorySaver()

// TODO use own traceMcp map instead cgat-stream one when refactoed
Expand Down Expand Up @@ -59,15 +54,6 @@ export default defineLazyEventHandler(() => {
})
}

// clean up token map as a bonus
const now = new Date()
for (const [key, value] of tokenMap.entries()) {
const diff = now.getTime() - value.date.getTime()
if (diff > 60 * 60 * 1000) {
tokenMap.delete(key)
}
}

const tokenHeader = getRequestHeader(event, 'authorization') || ''
if (tokenHeader) {
traceMcp('chat-stream: got Authorization header provided')
Expand Down Expand Up @@ -98,12 +84,6 @@ export default defineLazyEventHandler(() => {
traceLangchain('Starting new conversation with id:', conversationId)
}

traceMcp('set token map ', conversationId)
tokenMap.set(conversationId, {
date: new Date(),
token: ('' + tokenHeader).replace('Bearer ', ''),
})

const tools = []

if (appMcpServerUrl) {
Expand All @@ -113,7 +93,7 @@ export default defineLazyEventHandler(() => {
transport: 'http', // HTTP-based remote server
url: appMcpServerUrl,
headers: {
Authorization: `${conversationId}`,
Authorization: `${tokenHeader}`,
},
},
})
Expand Down
14 changes: 7 additions & 7 deletions src/server/routes/api/chatbot/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import checkAdminRights, { getUser } from '@/server/utils/check-admin-rights.js'
import traceLangchain from '@/server/projects-agent/tracers/trace-langchain'
import getAgentData from '@/server/projects-agent/agent/get-agent-data'
import getMetadata from '@/server/projects-agent/agent/get-metadata'
import { traceMcp } from '@/server/projects-agent/tracers/trace-mcp'
// import { traceMcp } from '@/server/projects-agent/tracers/trace-mcp'
import getAgent from '@/server/projects-agent/agent/get-agent'
import { tokenMap } from '@/server/routes/api/chat-stream'
import { v4 as uuidv4 } from 'uuid'
Expand Down Expand Up @@ -114,13 +114,13 @@ export default defineLazyEventHandler(() => {
traceLangchain('Starting new conversation with id:', conversationId)
}

traceMcp('set token map ', conversationId)
tokenMap.set(conversationId, {
date: new Date(),
token: ('' + tokenHeader).replace('Bearer ', ''),
})
// traceMcp('set token map ', conversationId)
// tokenMap.set(conversationId, {
// date: new Date(),
// token: ('' + tokenHeader).replace('Bearer ', ''),
// })

const agent = await getAgent(agentData, event, conversationId)
const agent = await getAgent(agentData, event, tokenHeader)

/* --------- Start coversation --------- */

Expand Down
37 changes: 37 additions & 0 deletions src/server/routes/authorize.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// server/routes/authorize.get.ts
import { traceMcp } from '@/server/projects-agent/tracers/trace-mcp'
import { applyMcpCors } from '@/server/utils/mcp-cors'

export default defineEventHandler(async (event) => {
if (applyMcpCors(event)) return
const { appKeycloakUrl, appKeycloakRealm, appMcpAllowedHosts } = useRuntimeConfig().public
const query = getQuery(event)
const ALLOWED_REDIRECT_HOSTS = ((appMcpAllowedHosts as string) || '').split('|')
traceMcp('/authorize hit with with query:\n', JSON.stringify(query, null, 2))

// Defensive: refuse to forward to Keycloak if redirect_uri isn't one we trust.
// This is an open-redirect guard — without it, this route would let anyone
// construct a Keycloak login flow that redirects the resulting code anywhere.
const redirectUri = query.redirect_uri as string | undefined
if (redirectUri) {
try {
const host = new URL(redirectUri).hostname
if (!ALLOWED_REDIRECT_HOSTS.includes(host)) {
traceMcp(
`Rejecting /authorize: disallowed redirect_uri host ${host} (allowed: ${ALLOWED_REDIRECT_HOSTS.map((host) => `"${host}"`).join(', ')}) `
)
throw createError({ statusCode: 400, statusMessage: 'invalid_redirect_uri' })
}
} catch (err) {
traceMcp('Error in /authorize', err)
throw createError({ statusCode: 400, statusMessage: 'invalid_redirect_uri' })
}
}

const ISSUER = `${appKeycloakUrl.replace(/\/$/, '')}/realms/${appKeycloakRealm}`
const qs = new URLSearchParams(query as Record<string, string>).toString()
const target = `${ISSUER}/protocol/openid-connect/auth?${qs}`

traceMcp('Redirecting /authorize to Keycloak', target)
return sendRedirect(event, target, 302)
})
Loading
Loading