From c3ac71f12bb0b5f3b8535be46b85d588ca7b3328 Mon Sep 17 00:00:00 2001 From: Guilherme Rodrigues Date: Thu, 1 Jan 2026 02:30:40 -0300 Subject: [PATCH 1/7] Add mesh token to on config --- apps/mesh/src/api/routes/proxy.ts | 15 +++++++++++++++ apps/mesh/src/tools/connection/update.ts | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/apps/mesh/src/api/routes/proxy.ts b/apps/mesh/src/api/routes/proxy.ts index d7f9ae8147..c4c0aa5d3f 100644 --- a/apps/mesh/src/api/routes/proxy.ts +++ b/apps/mesh/src/api/routes/proxy.ts @@ -703,6 +703,21 @@ async function createMCPProxyDoNotUseDirectly( getPrompt, }, callStreamableTool, + /** + * Get the configuration token for this proxy. + * This is the JWT that downstream MCPs can use to call back to Mesh. + * Useful for STDIO connections that can't receive headers per-request. + */ + getConfigurationToken: async (): Promise => { + await ensureConfigurationToken(); + return configurationToken; + }, + /** + * Get the Mesh URL that downstream MCPs should call back to. + */ + getMeshUrl: (): string => { + return process.env.MESH_URL ?? ctx.baseUrl; + }, }; } diff --git a/apps/mesh/src/tools/connection/update.ts b/apps/mesh/src/tools/connection/update.ts index d8d275f783..2334341eb9 100644 --- a/apps/mesh/src/tools/connection/update.ts +++ b/apps/mesh/src/tools/connection/update.ts @@ -185,11 +185,19 @@ export const COLLECTION_CONNECTIONS_UPDATE = defineTool({ ) { try { const proxy = await ctx.createMCPProxy(id); + // Get configuration token and mesh URL for STDIO connections + // HTTP connections receive these via headers, but STDIO needs them in arguments + const meshToken = await proxy.getConfigurationToken(); + const meshUrl = proxy.getMeshUrl(); + await proxy.client.callTool({ name: "ON_MCP_CONFIGURATION", arguments: { state: finalState, scopes: finalScopes, + // Include mesh context for STDIO connections that can't receive headers + meshToken, + meshUrl, }, }); } catch (error) { From f79d6b2324f70f30476d0b8096e5745794220753 Mon Sep 17 00:00:00 2001 From: Guilherme Rodrigues Date: Fri, 2 Jan 2026 14:25:44 -0300 Subject: [PATCH 2/7] Enhance documentation and implement STDIO connection support - Added documentation requirements to AGENTS.md for maintaining accurate documentation alongside code changes. - Updated README.md to include details on STDIO connections, including environment variable usage for credentials. - Expanded mcp-servers documentation in both English and Portuguese to clarify connection types and credential handling. - Modified proxy.ts to support infinite JWT tokens for STDIO connections and ensure environment variables are passed correctly. - Updated JWT handling to allow for no-expiration tokens for STDIO connections. - Improved logging in fetch-tools.ts for better debugging of STDIO tool fetch operations. - Added a refresh button in the settings tab to allow users to refresh tools from the MCP server. --- AGENTS.md | 23 +++++++ README.md | 17 +++++ .../src/content/en/mcp-mesh/mcp-servers.mdx | 64 ++++++++++++++++- .../content/pt-br/mcp-mesh/mcp-servers.mdx | 68 ++++++++++++++++++- apps/mesh/src/api/routes/proxy.ts | 66 ++++++++++++++---- apps/mesh/src/auth/jwt.ts | 25 +++++-- apps/mesh/src/tools/connection/fetch-tools.ts | 15 ++-- .../details/connection/settings-tab/index.tsx | 33 ++++++++- 8 files changed, 281 insertions(+), 30 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a7585f0837..5aab6899ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,11 +49,34 @@ Bun is the test runner throughout. Co-locate test files next to source as `*.tes ## Authentication & Authorization Uses Better Auth for authentication (OAuth 2.1 + SSO + API keys). Authorization uses custom AccessControl layer with organization/project-level RBAC. Auth configuration is in `apps/mesh/auth-config.json` (example: `auth-config.example.json`). +## Documentation Requirements + +**IMPORTANT**: Documentation must be updated alongside code changes. Before committing: + +1. **Check for relevant docs** in `apps/docs/client/src/content/` (EN + PT-BR) +2. **Update affected docs** when changing: + - API behavior or endpoints + - Configuration options + - Authentication/authorization flows + - Connection types or proxy behavior + - New features or removed functionality +3. **Update README.md** for significant features +4. **Review docs diff** before committing to ensure accuracy + +Documentation locations: +- `apps/docs/client/src/content/en/` - English docs +- `apps/docs/client/src/content/pt-br/` - Portuguese docs +- `README.md` - Project overview and quick start +- `packages/*/README.md` - Package-specific docs + +Run `bun run docs:dev` to preview documentation changes locally. + ## Commit & Pull Request Guidelines Follow Conventional Commit-style history: `type(scope): message`, optionally wrapping the type in brackets for chores (e.g., `[chore]: update deps`). Reference issues with `(#1234)` when applicable. PRs should include: - Succinct summary of changes - Testing notes and affected areas - Screenshots for UI changes +- **Documentation updates** for any behavior changes - Confirm formatting (`bun run fmt`) and linting (`bun run lint`) pass - Run tests (`bun test`) before requesting review - Flag follow-up work with TODOs linked to issues diff --git a/README.md b/README.md index fb3d61663b..d99f24cb71 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,23 @@ Gateways are configurable and extensible. You can add new strategies and also cu --- +## STDIO Connections (Local MCPs) + +Run npx packages or custom scripts as MCP servers. Mesh passes credentials via environment variables: + +```bash +# Mesh spawns your MCP with these env vars: +MESH_TOKEN= # Infinite-expiry JWT for mesh API calls +MESH_URL= # Mesh instance URL +MESH_STATE= # Binding values as JSON +``` + +Your MCP just reads `process.env.MESH_TOKEN` — no special configuration tools needed. This mirrors how HTTP connections receive `x-mesh-token` headers. + +→ See [Building STDIO MCPs](https://docs.deco.page/en/mcp-mesh/mcp-servers) for examples in [decocms/mcps](https://github.com/decocms/mcps). + +--- + ## Define Tools Tools are first-class citizens. Type-safe, audited, observable, and callable via MCP. diff --git a/apps/docs/client/src/content/en/mcp-mesh/mcp-servers.mdx b/apps/docs/client/src/content/en/mcp-mesh/mcp-servers.mdx index 8c7bc93ac0..19ddb53bf3 100644 --- a/apps/docs/client/src/content/en/mcp-mesh/mcp-servers.mdx +++ b/apps/docs/client/src/content/en/mcp-mesh/mcp-servers.mdx @@ -8,7 +8,69 @@ import Callout from "../../../components/ui/Callout.astro"; ## What is a connection? -An **MCP Server** in the Mesh is a configured upstream MCP endpoint (typically HTTP). The Mesh stores its configuration and (optionally) credentials, and can then proxy MCP requests to it. +An **MCP Server** in the Mesh is a configured upstream MCP endpoint. The Mesh stores its configuration and (optionally) credentials, and can then proxy MCP requests to it. + +## Connection Types + +The Mesh supports two types of connections: + +### HTTP Connections + +HTTP connections are the most common type. They connect to remote MCP servers via HTTP/SSE endpoints. + +- **Use cases**: Cloud-hosted MCP servers, SaaS integrations, production deployments +- **Token handling**: Short-lived tokens (5 minutes) issued per request + +### STDIO Connections (Local Commands) + +STDIO connections spawn a local process (like `npx` or custom scripts) and communicate via stdin/stdout. + +- **Use cases**: Local tools, npx packages, development, private MCPs +- **Token handling**: Infinite-expiry tokens persisted locally by the MCP server + + + STDIO connections are perfect for running npm packages as MCP servers. Example: `npx @decocms/local-fs` runs a file system MCP locally. + + +## STDIO Credentials via Environment Variables + +When mesh spawns an STDIO MCP process, it passes credentials as environment variables: + +| Variable | Description | +|----------|-------------| +| `MESH_TOKEN` | JWT token for authenticating with Mesh API (infinite expiry) | +| `MESH_URL` | Base URL of the Mesh instance | +| `MESH_STATE` | JSON-encoded state with binding values | + +This is analogous to how HTTP connections receive `x-mesh-token` headers. + + + **No special tools needed!** Just read `process.env.MESH_TOKEN` on startup. No need to implement `ON_MCP_CONFIGURATION` or any configuration protocol. + + +### Example (Node.js/Bun) + +```typescript +// Read mesh credentials from env +const meshToken = process.env.MESH_TOKEN; +const meshUrl = process.env.MESH_URL; +const state = process.env.MESH_STATE ? JSON.parse(process.env.MESH_STATE) : {}; + +// Use token for mesh API calls +const response = await fetch(`${meshUrl}/mcp/${connectionId}`, { + headers: { Authorization: `Bearer ${meshToken}` }, + // ... +}); +``` + +## Building STDIO-Compatible MCPs + +See examples in the [decocms/mcps](https://github.com/decocms/mcps) repository: + +- `template-minimal/` - Minimal MCP without view +- `template-with-view/` - MCP with web interface +- `local-fs/` - File system MCP (runs via npx) +- `perplexity/`, `openrouter/` - Production MCPs ## In the UI diff --git a/apps/docs/client/src/content/pt-br/mcp-mesh/mcp-servers.mdx b/apps/docs/client/src/content/pt-br/mcp-mesh/mcp-servers.mdx index 91811aab93..ae278e5a1c 100644 --- a/apps/docs/client/src/content/pt-br/mcp-mesh/mcp-servers.mdx +++ b/apps/docs/client/src/content/pt-br/mcp-mesh/mcp-servers.mdx @@ -1,24 +1,86 @@ --- title: MCP Servers -description: Conexões com MCP servers upstream (HTTP) que o Mesh faz proxy e observa +description: Conexões com MCP servers upstream que o Mesh faz proxy e observa icon: Server --- +import Callout from "../../../components/ui/Callout.astro"; + ## O que é um MCP Server (no Mesh)? -No Mesh, um **MCP Server** é uma **connection** para um MCP upstream (normalmente um endpoint MCP via HTTP). +No Mesh, um **MCP Server** é uma **connection** para um MCP upstream. Cada connection guarda: -- **URL do MCP** +- **URL do MCP** (para HTTP) ou **comando** (para STDIO) - **credenciais/config** (quando necessário, armazenadas no vault) - metadados para operação (nome, status, etc.) +## Tipos de Conexão + +### Conexões HTTP + +Conexões HTTP conectam a servidores MCP remotos via endpoints HTTP/SSE. + +- **Casos de uso**: MCPs em nuvem, integrações SaaS, produção +- **Tokens**: JWT com expiração curta (5 minutos) + +### Conexões STDIO (Comandos Locais) + +Conexões STDIO iniciam um processo local (como `npx` ou scripts) e comunicam via stdin/stdout. + +- **Casos de uso**: Ferramentas locais, pacotes npx, desenvolvimento +- **Tokens**: JWT sem expiração, persistido localmente + + + Conexões STDIO são perfeitas para rodar pacotes npm como MCPs. Exemplo: `npx @decocms/local-fs` roda um MCP de sistema de arquivos localmente. + + +## Credenciais STDIO via Variáveis de Ambiente + +Quando o mesh inicia um processo MCP STDIO, ele passa credenciais como variáveis de ambiente: + +| Variável | Descrição | +|----------|-----------| +| `MESH_TOKEN` | Token JWT para autenticação com a API do Mesh (sem expiração) | +| `MESH_URL` | URL base da instância Mesh | +| `MESH_STATE` | Estado com valores de bindings em JSON | + +Isso é análogo a como conexões HTTP recebem headers `x-mesh-token`. + + + **Nenhuma ferramenta especial necessária!** Apenas leia `process.env.MESH_TOKEN` na inicialização. Não precisa implementar `ON_MCP_CONFIGURATION` ou qualquer protocolo de configuração. + + +### Exemplo (Node.js/Bun) + +```typescript +// Ler credenciais mesh das env vars +const meshToken = process.env.MESH_TOKEN; +const meshUrl = process.env.MESH_URL; +const state = process.env.MESH_STATE ? JSON.parse(process.env.MESH_STATE) : {}; + +// Usar token para chamadas à API mesh +const response = await fetch(`${meshUrl}/mcp/${connectionId}`, { + headers: { Authorization: `Bearer ${meshToken}` }, + // ... +}); +``` + ## Quando usar vs Gateway - Use **connection** para isolar e depurar um upstream específico. - Use **gateway** para expor um surface agregado/curado para os clients. +## Construindo MCPs STDIO + +Veja exemplos no repositório [decocms/mcps](https://github.com/decocms/mcps): + +- `template-minimal/` - MCP mínimo sem view +- `template-with-view/` - MCP com interface web +- `local-fs/` - MCP de sistema de arquivos (roda via npx) +- `perplexity/`, `openrouter/` - MCPs em produção + ## Boas práticas - Mantenha URLs e credenciais por ambiente (dev/staging/prod). diff --git a/apps/mesh/src/api/routes/proxy.ts b/apps/mesh/src/api/routes/proxy.ts index c4c0aa5d3f..646e069c5f 100644 --- a/apps/mesh/src/api/routes/proxy.ts +++ b/apps/mesh/src/api/routes/proxy.ts @@ -221,8 +221,9 @@ async function createMCPProxyDoNotUseDirectly( connection.configuration_scopes, ); - // Issue short-lived JWT with configuration permissions - // JWT can be decoded directly by downstream to access payload + // Issue JWT with configuration permissions + // HTTP connections get 5-min tokens, STDIO connections get infinite tokens + // STDIO servers persist tokens locally to .env for restart survival const userId = ctx.auth.user?.id ?? ctx.auth.apiKey?.userId; if (!userId) { console.error("User ID required to issue configuration token"); @@ -230,17 +231,24 @@ async function createMCPProxyDoNotUseDirectly( } try { - configurationToken = await issueMeshToken({ - sub: userId, - user: { id: userId }, - metadata: { - state: connection.configuration_state ?? undefined, - meshUrl: process.env.MESH_URL ?? ctx.baseUrl, - connectionId, - organizationId: ctx.organization?.id, + // STDIO connections get infinite tokens - they persist them locally to .env + // This avoids the need to re-send ON_MCP_CONFIGURATION on every request + const isStdioConnection = connection.connection_type === "STDIO"; + + configurationToken = await issueMeshToken( + { + sub: userId, + user: { id: userId }, + metadata: { + state: connection.configuration_state ?? undefined, + meshUrl: process.env.MESH_URL ?? ctx.baseUrl, + connectionId, + organizationId: ctx.organization?.id, + }, + permissions, }, - permissions, - }); + { noExpiration: isStdioConnection }, + ); } catch (error) { console.error("Failed to issue configuration token:", error); // Continue without configuration token - downstream will fail if it requires it @@ -279,6 +287,30 @@ async function createMCPProxyDoNotUseDirectly( ? (connection.connection_headers as HttpConnectionParameters | null) : null; + // Build env vars for STDIO connections (token + state passed via env) + const buildStdioEnv = async (): Promise> => { + await ensureConfigurationToken(); + const meshUrl = process.env.MESH_URL ?? ctx.baseUrl; + + const env: Record = {}; + + // Pass mesh credentials via env vars - STDIO servers just read these + if (configurationToken) { + env.MESH_TOKEN = configurationToken; + } + if (meshUrl) { + env.MESH_URL = meshUrl; + } + + // Pass state as JSON for bindings + const state = connection.configuration_state; + if (state && Object.keys(state).length > 0) { + env.MESH_STATE = JSON.stringify(state); + } + + return env; + }; + // Create client factory for downstream MCP based on connection_type const createClient = async () => { switch (connection.connection_type) { @@ -297,16 +329,22 @@ async function createMCPProxyDoNotUseDirectly( throw new Error("STDIO connection missing parameters"); } + // Build env with mesh credentials - STDIO servers read MESH_TOKEN/MESH_URL/MESH_STATE + const meshEnv = await buildStdioEnv(); + const env = { ...stdioParams.envVars, ...meshEnv }; + // Get or create stable connection - respawns automatically if closed // We want stable local MCP connection - don't spawn new process per request - return getStableStdioClient({ + const client = await getStableStdioClient({ id: connectionId, name: connection.title, command: stdioParams.command, args: stdioParams.args, - env: stdioParams.envVars, + env, cwd: stdioParams.cwd, }); + + return client; } case "HTTP": diff --git a/apps/mesh/src/auth/jwt.ts b/apps/mesh/src/auth/jwt.ts index a1c60ae808..7ef728b89d 100644 --- a/apps/mesh/src/auth/jwt.ts +++ b/apps/mesh/src/auth/jwt.ts @@ -66,24 +66,37 @@ export interface MeshTokenPayload { export type MeshJwtPayload = JWTPayload & MeshTokenPayload; +export interface IssueMeshTokenOptions { + /** Expiration time (default: "5m"). Ignored if noExpiration is true. */ + expiresIn?: string; + /** If true, issues a token with no expiration. Use for STDIO connections. */ + noExpiration?: boolean; +} + /** * Issue a signed JWT with mesh token payload * * @param payload - The token payload - * @param expiresIn - Expiration time (default: 5 minutes) + * @param options - Token options (expiresIn, noExpiration) * @returns Signed JWT string */ export async function issueMeshToken( payload: MeshTokenPayload, - expiresIn: string = "5m", + options: IssueMeshTokenOptions = {}, ): Promise { + const { expiresIn = "5m", noExpiration = false } = options; const secret = getSecret(); - return await new SignJWT(payload as unknown as JWTPayload) + const jwt = new SignJWT(payload as unknown as JWTPayload) .setProtectedHeader({ alg: "HS256", typ: "JWT" }) - .setIssuedAt() - .setExpirationTime(expiresIn) - .sign(secret); + .setIssuedAt(); + + // STDIO connections get infinite tokens - they persist them locally + if (!noExpiration) { + jwt.setExpirationTime(expiresIn); + } + + return await jwt.sign(secret); } /** diff --git a/apps/mesh/src/tools/connection/fetch-tools.ts b/apps/mesh/src/tools/connection/fetch-tools.ts index e92f95e84f..3bfe9c8e0e 100644 --- a/apps/mesh/src/tools/connection/fetch-tools.ts +++ b/apps/mesh/src/tools/connection/fetch-tools.ts @@ -164,8 +164,18 @@ async function fetchToolsFromStdioMCP( setTimeout(() => reject(new Error("Tool fetch timeout")), 10_000); }); + console.log( + `[STDIO tool fetch] Connecting to ${connection.id}: ${stdioParams.command} ${stdioParams.args?.join(" ")}`, + ); + await Promise.race([client.connect(transport), timeoutPromise]); + console.log(`[STDIO tool fetch] Connected, listing tools...`); + const result = await Promise.race([client.listTools(), timeoutPromise]); + console.log( + `[STDIO tool fetch] Got ${result.tools?.length ?? 0} tools:`, + result.tools?.map((t) => t.name), + ); if (!result.tools || result.tools.length === 0) { return null; @@ -178,10 +188,7 @@ async function fetchToolsFromStdioMCP( outputSchema: tool.outputSchema ?? undefined, })); } catch (error) { - console.error( - `Failed to fetch tools from STDIO connection ${connection.id}:`, - error, - ); + console.error(`[STDIO tool fetch] Failed for ${connection.id}:`, error); return null; } finally { try { diff --git a/apps/mesh/src/web/components/details/connection/settings-tab/index.tsx b/apps/mesh/src/web/components/details/connection/settings-tab/index.tsx index 27344eb015..af7cd9223e 100644 --- a/apps/mesh/src/web/components/details/connection/settings-tab/index.tsx +++ b/apps/mesh/src/web/components/details/connection/settings-tab/index.tsx @@ -18,10 +18,10 @@ import { useToolCall } from "@/web/hooks/use-tool-call"; import { authenticateMcp } from "@/web/lib/mcp-oauth"; import { KEYS } from "@/web/lib/query-keys"; import { Button } from "@deco/ui/components/button.tsx"; -import { Key01, File06, Loading01 } from "@untitledui/icons"; +import { Key01, File06, Loading01, RefreshCw01 } from "@untitledui/icons"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQueryClient } from "@tanstack/react-query"; -import { Suspense } from "react"; +import { Suspense, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { ViewActions } from "../../layout"; @@ -429,6 +429,7 @@ function SettingsTabContentImpl(props: SettingsTabContentImplProps) { const connectionActions = useConnectionActions(); const queryClient = useQueryClient(); + const [isRefreshing, setIsRefreshing] = useState(false); // Check if connection has README const repository = connection?.metadata?.repository as @@ -484,9 +485,37 @@ function SettingsTabContentImpl(props: SettingsTabContentImplProps) { toast.success("Authentication successful"); }; + const handleRefreshTools = async () => { + setIsRefreshing(true); + try { + // Trigger an update with no changes to force tool refresh + // Note: connectionActions.update handles success/error toasts via onSuccess/onError + await connectionActions.update.mutateAsync({ + id: connection.id, + data: {}, + }); + } finally { + setIsRefreshing(false); + } + }; + return ( <> + {hasAnyChanges && (