Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Database migrations live in `apps/mesh/migrations/`, code quality plugins in `pl
- `bun run build:runtime`: build the runtime package
- `bun run docs:dev`: run documentation site locally

**IMPORTANT**: Always run `bun run fmt` after making code changes to ensure consistent formatting. A lefthook pre-commit hook is configured to run this automatically. Install with `npx lefthook install`.

### Mesh-specific commands (from `apps/mesh/`)
- `bun run dev:client`: Vite dev server (port 4000)
- `bun run dev:server`: Hono server with hot reload
Expand Down
11 changes: 11 additions & 0 deletions apps/mesh/src/api/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export type AuthConfig = {
| {
enabled: false;
};
/**
* Whether STDIO connections are allowed.
* Disabled by default in production unless UNSAFE_ALLOW_STDIO_TRANSPORT=true
*/
stdioEnabled: boolean;
};

/**
Expand All @@ -51,6 +56,11 @@ app.get("/config", async (c) => {
icon: KNOWN_OAUTH_PROVIDERS[name as OAuthProvider].icon,
}));

// STDIO is disabled in production unless explicitly allowed
const stdioEnabled =
process.env.NODE_ENV !== "production" ||
process.env.UNSAFE_ALLOW_STDIO_TRANSPORT === "true";

const config: AuthConfig = {
emailAndPassword: {
enabled: authConfig.emailAndPassword?.enabled ?? false,
Expand All @@ -70,6 +80,7 @@ app.get("/config", async (c) => {
: {
enabled: false,
},
stdioEnabled,
};

return c.json({ success: true, config });
Expand Down
132 changes: 103 additions & 29 deletions apps/mesh/src/api/routes/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@
* - Creates MCP Server to handle incoming requests
Copy link

@cubic-dev-ai cubic-dev-ai bot Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Missing custom headers in streamable tool calls. The HTTP client path adds httpParams?.headers after buildRequestHeaders(), but callStreamableTool (which only works for HTTP connections per the comment) doesn't add these headers. This could cause authentication or authorization failures for connections that depend on custom headers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/proxy.ts, line 498:

<comment>Missing custom headers in streamable tool calls. The HTTP client path adds `httpParams?.headers` after `buildRequestHeaders()`, but `callStreamableTool` (which only works for HTTP connections per the comment) doesn&#39;t add these headers. This could cause authentication or authorization failures for connections that depend on custom headers.</comment>

<file context>
@@ -483,10 +495,16 @@ async function createMCPProxyDoNotUseDirectly(
 
   // Call tool using fetch directly for streaming support
   // Inspired by @deco/api proxy callStreamableTool
+  // Note: Only works for HTTP connections - STDIO doesn&#39;t support streaming fetch
   const callStreamableTool = async (
     name: string,
</file context>

✅ Addressed in 230338a

* - Creates MCP Client to connect to downstream connections
* - Uses middleware pipeline for authorization
* - Supports StreamableHTTP transport
* - Supports StreamableHTTP and STDIO transports
*/

import { extractConnectionPermissions } from "@/auth/configuration-scopes";
import { once } from "@/common";
import { getMonitoringConfig } from "@/core/config";
import { ConnectionEntity } from "@/tools/connection/schema";
import { getStableStdioClient } from "@/stdio/stable-transport";
import {
ConnectionEntity,
isStdioParameters,
type HttpConnectionParameters,
} from "@/tools/connection/schema";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
Expand Down Expand Up @@ -263,33 +268,87 @@ async function createMCPProxyDoNotUseDirectly(
headers["x-mesh-token"] = configurationToken;
}

// Add custom headers from connection
if (connection.connection_headers) {
Object.assign(headers, connection.connection_headers);
}

return headers;
};

// Create client factory for downstream MCP
// Determine connection type and extract parameters
const isStdio = connection.connection_type === "STDIO";
const stdioParams = isStdioParameters(connection.connection_headers)
? connection.connection_headers
: null;
const httpParams = !isStdio
? (connection.connection_headers as HttpConnectionParameters | null)
: null;

// Create client factory for downstream MCP based on connection_type
const createClient = async () => {
const headers = await buildRequestHeaders();
switch (connection.connection_type) {
case "STDIO": {
// Block STDIO connections in production unless explicitly allowed
if (
process.env.NODE_ENV === "production" &&
process.env.UNSAFE_ALLOW_STDIO_TRANSPORT !== "true"
) {
throw new Error(
"STDIO connections are disabled in production. Set UNSAFE_ALLOW_STDIO_TRANSPORT=true to enable.",
);
}

// Create transport to downstream MCP using StreamableHTTP
const transport = new StreamableHTTPClientTransport(
new URL(connection.connection_url),
{ requestInit: { headers } },
);
if (!stdioParams) {
throw new Error("STDIO connection missing parameters");
}

// Create MCP client
const client = new Client({
name: "mcp-mesh-proxy",
version: "1.0.0",
});
// Get or create stable connection - respawns automatically if closed
// We want stable local MCP connection - don't spawn new process per request
return getStableStdioClient({
id: connectionId,
name: connection.title,
command: stdioParams.command,
args: stdioParams.args,
env: stdioParams.envVars,
cwd: stdioParams.cwd,
});
}

case "HTTP":
case "SSE":
case "Websocket": {
if (!connection.connection_url) {
throw new Error(
`${connection.connection_type} connection missing URL`,
);
}

// HTTP/SSE/WebSocket - create fresh client per request
const client = new Client({
name: "mcp-mesh-proxy",
version: "1.0.0",
});

await client.connect(transport);
const headers = await buildRequestHeaders();

return client;
// Add custom headers from connection_headers
if (httpParams?.headers) {
Object.assign(headers, httpParams.headers);
}

// Create transport to downstream MCP using StreamableHTTP
// TODO: Add SSE transport support when needed
const transport = new StreamableHTTPClientTransport(
new URL(connection.connection_url),
{ requestInit: { headers } },
);

await client.connect(transport);

return client;
}

default:
throw new Error(
`Unknown connection type: ${connection.connection_type}`,
);
}
};

// Create authorization middlewares
Expand Down Expand Up @@ -385,6 +444,7 @@ async function createMCPProxyDoNotUseDirectly(

throw error;
} finally {
// Close client - stdio connections ignore close() via stable-transport
await client.close();
}
},
Expand Down Expand Up @@ -449,20 +509,31 @@ async function createMCPProxyDoNotUseDirectly(

// Call tool using fetch directly for streaming support
// Inspired by @deco/api proxy callStreamableTool
// Note: Only works for HTTP connections - STDIO doesn't support streaming fetch
const callStreamableTool = async (
name: string,
args: Record<string, unknown>,
): Promise<Response> => {
if (!connection.connection_url) {
throw new Error("Streamable tools require HTTP connection with URL");
}
const connectionUrl = connection.connection_url;

const request: CallToolRequest = {
method: "tools/call",
params: { name, arguments: args },
};
return callStreamableToolPipeline(request, async (): Promise<Response> => {
const headers = await buildRequestHeaders();

// Add custom headers from connection_headers
if (httpParams?.headers) {
Object.assign(headers, httpParams.headers);
}

// Use fetch directly to support streaming responses
// Build URL with tool name appended for call-tool endpoint pattern
const url = new URL(connection.connection_url);
const url = new URL(connectionUrl);
url.pathname =
url.pathname.replace(/\/$/, "") + `/call-tool/${request.params.name}`;

Expand Down Expand Up @@ -546,13 +617,16 @@ async function createMCPProxyDoNotUseDirectly(
client = await createClient();
} catch (error) {
// Check if this is an auth error - if so, return appropriate 401
const authResponse = await handleAuthError({
error: error as Error & { status?: number },
reqUrl,
connectionId,
connectionUrl: connection.connection_url,
headers: await buildRequestHeaders(),
});
// Note: This only applies to HTTP connections
const authResponse = connection.connection_url
? await handleAuthError({
error: error as Error & { status?: number },
reqUrl,
connectionId,
connectionUrl: connection.connection_url,
headers: await buildRequestHeaders(),
})
: null;
if (authResponse) {
return authResponse;
}
Expand Down
8 changes: 8 additions & 0 deletions apps/mesh/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,14 @@ export const auth = betterAuth({
// Load optional configuration from file
...authConfig,

// Disable rate limiting in development (set DISABLE_RATE_LIMIT=true)
// Must be AFTER authConfig spread to ensure it takes precedence
rateLimit: {
enabled: process.env.DISABLE_RATE_LIMIT !== "true",
window: 60,
max: 10000, // Very high limit as fallback
},

plugins,

// Database hooks for automatic organization creation on signup
Expand Down
1 change: 1 addition & 0 deletions apps/mesh/src/auth/org.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export async function seedOrgDb(organizationId: string, createdBy: string) {
(await fetchToolsFromMCP({
id: "pending",
title: mcpConfig.data.title,
connection_type: mcpConfig.data.connection_type,
connection_url: mcpConfig.data.connection_url,
connection_token: mcpConfig.data.connection_token,
connection_headers: mcpConfig.data.connection_headers,
Expand Down
11 changes: 9 additions & 2 deletions apps/mesh/src/core/context-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,15 @@ async function authenticateRequest(
};
}
} catch (error) {
const err = error as Error;
console.error("[Auth] Session check failed:", err);
const err = error as Error & { body?: unknown };
console.error(
"[Auth] Session check failed:",
JSON.stringify(
{ message: err.message, body: err.body, stack: err.stack },
null,
2,
),
);
}

// No valid authentication found - return empty auth data
Expand Down
Loading