Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/mesh/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@ai-sdk/provider": "^3.0.8",
"@electric-sql/pglite": "^0.3.15",
"@ai-sdk/react": "^3.0.118",
"@aws-sdk/client-s3": "^3.1010.0",
"@better-auth/sso": "1.4.1",
"@daveyplate/better-auth-ui": "^3.2.7",
"@deco/ui": "workspace:*",
Expand Down
43 changes: 42 additions & 1 deletion apps/mesh/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { env } from "../env";
import { DECO_STORE_URL, isDecoHostedMcp } from "@/core/deco-constants";
import { WellKnownOrgMCPId } from "@decocms/mesh-sdk";
import { PrometheusSerializer } from "@opentelemetry/exporter-prometheus";
import { Hono } from "hono";
import { type Context, Hono } from "hono";
import { getCookie } from "hono/cookie";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
Expand Down Expand Up @@ -956,6 +956,47 @@ export async function createApp(options: CreateAppOptions = {}) {
mountDevRoutes(app, mcpAuth);
}

// Filesystem MCP routes (S3-backed filesystem for agents)
// Handle /mcp/filesystem alias
app.use("/mcp/filesystem", mcpAuth);
app.route(
"/mcp/filesystem",
(await import("./routes/filesystem-mcp")).default,
);

// Handle {org_id}_filesystem connection ID pattern
app.all(
"/mcp/:connectionId{.*_filesystem$}",
mcpAuth,
async (c: Context<Env>) => {
const ctx = c.get("meshContext") as MeshContext;
const { handleFilesystemMcpRequest } = await import(
"./routes/filesystem-mcp"
);
return handleFilesystemMcpRequest(c.req.raw, ctx);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

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

P1: Add connection/tool authorization before executing filesystem MCP requests; these routes currently bypass the normal AccessControl checks and expose org file operations to any authenticated caller.

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

<comment>Add connection/tool authorization before executing filesystem MCP requests; these routes currently bypass the normal AccessControl checks and expose org file operations to any authenticated caller.</comment>

<file context>
@@ -971,6 +971,47 @@ export async function createApp(options: CreateAppOptions = {}) {
+      const { handleFilesystemMcpRequest } = await import(
+        "./routes/filesystem-mcp"
+      );
+      return handleFilesystemMcpRequest(c.req.raw, ctx);
+    },
+  );
</file context>
Fix with Cubic

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

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

P2: Validate :connectionId against the active organization before serving *_filesystem requests; right now any suffix-matching ID is accepted and routed to the current org’s filesystem.

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

<comment>Validate `:connectionId` against the active organization before serving `*_filesystem` requests; right now any suffix-matching ID is accepted and routed to the current org’s filesystem.</comment>

<file context>
@@ -971,6 +971,47 @@ export async function createApp(options: CreateAppOptions = {}) {
+      const { handleFilesystemMcpRequest } = await import(
+        "./routes/filesystem-mcp"
+      );
+      return handleFilesystemMcpRequest(c.req.raw, ctx);
+    },
+  );
</file context>
Suggested change
return handleFilesystemMcpRequest(c.req.raw, ctx);
const connectionId = c.req.param("connectionId");
if (
ctx.organization?.id &&
connectionId !== WellKnownOrgMCPId.FILESYSTEM(ctx.organization.id)
) {
return c.json({ error: "Connection not found" }, 404);
}
return handleFilesystemMcpRequest(c.req.raw, ctx);
Fix with Cubic

},
);

// Handle call-tool endpoint for filesystem connections
app.all(
"/mcp/:connectionId{.*_filesystem$}/call-tool/:toolName",
mcpAuth,
async (c: Context<Env>) => {
const ctx = c.get("meshContext") as MeshContext;
const toolName = c.req.param("toolName");
if (!toolName) {
return c.json({ error: "Missing tool name" }, 400);
}
const args = (await c.req.json()) as Record<string, unknown>;
const { callFilesystemTool } = await import("./routes/filesystem-mcp");
const result = await callFilesystemTool(toolName, args, ctx);
if (result.isError) {
return c.json(result.content, 500);
}
return c.json(result.content);
},
);

// Virtual MCP / Agent routes (must be before proxy to match /mcp/gateway and /mcp/virtual-mcp before /mcp/:connectionId)
// /mcp/gateway/:virtualMcpId (backward compat) or /mcp/virtual-mcp/:virtualMcpId
app.route("/mcp", virtualMcpRoutes);
Expand Down
306 changes: 306 additions & 0 deletions apps/mesh/src/api/routes/filesystem-mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
/**
* Filesystem MCP Server
*
* An MCP server that implements the FILESYSTEM_BINDING interface
* using S3-compatible storage as the backing store.
*
* Provides inline content access for AI agents (read/write file content
* directly in tool calls), unlike OBJECT_STORAGE_BINDING which uses presigned URLs.
*
* Route: POST /mcp/filesystem
* Also handles org-scoped connection ID pattern: /mcp/{orgId}_filesystem
*/

import type {
FsDeleteInput,
FsDeleteOutput,
FsListInput,
FsListOutput,
FsMetadataInput,
FsMetadataOutput,
FsReadInput,
FsReadOutput,
FsWriteInput,
FsWriteOutput,
} from "@decocms/bindings/filesystem";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { Hono } from "hono";
import { z } from "zod";
import type { MeshContext } from "../../core/mesh-context";
import { requireOrganization } from "../../core/mesh-context";
import { S3Service } from "../../filesystem/s3-service";
import {
FsDeleteInputSchema,
FsDeleteOutputSchema,
FsListInputSchema,
FsListOutputSchema,
FsMetadataInputSchema,
FsMetadataOutputSchema,
FsReadInputSchema,
FsReadOutputSchema,
FsWriteInputSchema,
FsWriteOutputSchema,
} from "../../tools/filesystem/schema";
import { getFilesystemS3Service } from "../../filesystem/factory";

// Local tool definition type
interface ToolDefinition {
name: string;
description?: string;
inputSchema: z.ZodTypeAny;
outputSchema?: z.ZodTypeAny;
handler: (args: Record<string, unknown>) => Promise<unknown>;
annotations?: {
title?: string;
readOnlyHint?: boolean;
destructiveHint?: boolean;
idempotentHint?: boolean;
openWorldHint?: boolean;
};
}

// Define Hono variables type
type Variables = {
meshContext: MeshContext;
};

const app = new Hono<{ Variables: Variables }>();

function createFilesystemTools(s3: S3Service, orgId: string): ToolDefinition[] {
return [
{
name: "FS_READ",
description:
"Read file content. Returns content inline for text files and small binary files (as base64). Returns an error for files exceeding the size limit.",
inputSchema: FsReadInputSchema,
outputSchema: FsReadOutputSchema,
annotations: {
title: "Read File",
readOnlyHint: true,
destructiveHint: false,
},
handler: async (args): Promise<FsReadOutput> => {
const input = args as FsReadInput;
return s3.readFile(orgId, input.path, input.offset, input.limit);
},
},
{
name: "FS_WRITE",
description:
"Write content to a file. Creates the file if it doesn't exist, overwrites if it does.",
inputSchema: FsWriteInputSchema,
outputSchema: FsWriteOutputSchema,
annotations: {
title: "Write File",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
},
handler: async (args): Promise<FsWriteOutput> => {
const input = args as FsWriteInput;
return s3.writeFile(
orgId,
input.path,
input.content,
input.encoding,
input.contentType,
);
},
},
{
name: "FS_LIST",
description:
"List files and directories at a given path. Supports pagination and glob pattern filtering on file names.",
inputSchema: FsListInputSchema,
outputSchema: FsListOutputSchema,
annotations: {
title: "List Files",
readOnlyHint: true,
destructiveHint: false,
},
handler: async (args): Promise<FsListOutput> => {
const input = args as FsListInput;
return s3.listFiles(orgId, input);
},
},
{
name: "FS_DELETE",
description: "Delete a single file.",
inputSchema: FsDeleteInputSchema,
outputSchema: FsDeleteOutputSchema,
annotations: {
title: "Delete File",
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
},
handler: async (args): Promise<FsDeleteOutput> => {
const input = args as FsDeleteInput;
return s3.deleteFile(orgId, input.path);
},
},
{
name: "FS_METADATA",
description:
"Get file metadata including size, content type, last modified time, and ETag.",
inputSchema: FsMetadataInputSchema,
outputSchema: FsMetadataOutputSchema,
annotations: {
title: "File Metadata",
readOnlyHint: true,
destructiveHint: false,
},
handler: async (args): Promise<FsMetadataOutput> => {
const input = args as FsMetadataInput;
return s3.getMetadata(orgId, input.path);
},
},
];
}

/**
* Handle a filesystem MCP request with a given context
*/
export async function handleFilesystemMcpRequest(
req: Request,
ctx: MeshContext,
): Promise<Response> {
const org = requireOrganization(ctx);
const s3 = getFilesystemS3Service();

if (!s3) {
return new Response(
JSON.stringify({
error: "Filesystem not configured. Set S3_* environment variables.",
}),
{ status: 503, headers: { "Content-Type": "application/json" } },
);
}

const tools = createFilesystemTools(s3, org.id);

const server = new McpServer(
{ name: "filesystem-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } },
);

for (const tool of tools) {
const inputShape =
"shape" in tool.inputSchema
? (tool.inputSchema.shape as z.ZodRawShape)
: z.object({}).shape;
const outputShape =
tool.outputSchema && "shape" in tool.outputSchema
? (tool.outputSchema.shape as z.ZodRawShape)
: z.object({}).shape;

server.registerTool(
tool.name,
{
description: tool.description ?? "",
inputSchema: inputShape,
outputSchema: outputShape,
annotations: tool.annotations,
},
async (args) => {
try {
const result = await tool.handler(args);
return {
content: [{ type: "text" as const, text: JSON.stringify(result) }],
structuredContent: result as { [x: string]: unknown },
};
} catch (error) {
const err = error as Error;
return {
content: [{ type: "text" as const, text: `Error: ${err.message}` }],
isError: true,
};
}
},
);
}

const transport = new WebStandardStreamableHTTPServerTransport({
enableJsonResponse:
req.headers.get("Accept")?.includes("application/json") ?? false,
});
await server.connect(transport);
return transport.handleRequest(req);
}

/**
* Call a filesystem tool directly
*/
export async function callFilesystemTool(
toolName: string,
args: Record<string, unknown>,
ctx: MeshContext,
): Promise<{ content: unknown; isError?: boolean }> {
const org = requireOrganization(ctx);
const s3 = getFilesystemS3Service();

if (!s3) {
return {
content: [
{
type: "text",
text: "Filesystem not configured. Set S3_* environment variables.",
},
],
isError: true,
};
}

const tools = createFilesystemTools(s3, org.id);
const tool = tools.find((t) => t.name === toolName);

if (!tool) {
return {
content: [{ type: "text", text: `Tool not found: ${toolName}` }],
isError: true,
};
}

const parsed = tool.inputSchema.safeParse(args);
if (!parsed.success) {
return {
content: [
{
type: "text",
text: `Invalid input: ${parsed.error.message}`,
},
],
isError: true,
};
}

try {
const result = await tool.handler(parsed.data as Record<string, unknown>);
return {
content: [{ type: "text", text: JSON.stringify(result) }],
};
} catch (error) {
return {
content: [
{
type: "text",
text: error instanceof Error ? error.message : String(error),
},
],
isError: true,
};
}
}

/**
* Filesystem MCP endpoint
*
* Route: POST /mcp/filesystem
*/
app.all("/", async (c) => {
const ctx = c.get("meshContext");
return handleFilesystemMcpRequest(c.req.raw, ctx);
});

export default app;
Loading
Loading