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
5 changes: 5 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"permissions": {
"allow": ["Bash(git remote prune:*)"]
}
}
18 changes: 12 additions & 6 deletions apps/mesh/src/aggregator/resource-aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,26 +96,32 @@ export class ResourceAggregator {
let resources = result.resources;

// Apply selection based on mode
// Note: ui:// resources (MCP Apps) are always included regardless of selection mode
if (this.options.selectionMode === "exclusion") {
// Exclusion mode: exclude matching resources
// Exclusion mode: exclude matching resources (but always keep ui:// resources)
if (entry.selectedResources && entry.selectedResources.length > 0) {
resources = resources.filter(
(r) => !matchesAnyPattern(r.uri, entry.selectedResources!),
(r) =>
r.uri.startsWith("ui://") ||
!matchesAnyPattern(r.uri, entry.selectedResources!),
);
}
// If selectedResources is null/empty in exclusion mode, include all resources
} else {
// Inclusion mode: include only selected resources
// Resources require explicit selection (patterns or URIs)
// Exception: ui:// resources (MCP Apps) are always included
if (
!entry.selectedResources ||
entry.selectedResources.length === 0
) {
// No resources selected = no resources from this connection
resources = [];
// No resources selected = only include ui:// resources
resources = resources.filter((r) => r.uri.startsWith("ui://"));
} else {
resources = resources.filter((r) =>
matchesAnyPattern(r.uri, entry.selectedResources!),
resources = resources.filter(
(r) =>
r.uri.startsWith("ui://") ||
matchesAnyPattern(r.uri, entry.selectedResources!),
);
}
}
Expand Down
17 changes: 12 additions & 5 deletions apps/mesh/src/aggregator/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,18 @@ function createSearchTool(ctx: StrategyContext): ToolWithHandler {
);
return jsonResult({
query: parsed.data.query,
results: results.map((t) => ({
name: t.name,
description: t.description,
connection: t._meta.connectionTitle,
})),
results: results.map((t) => {
const meta = t._meta as Record<string, unknown>;
return {
name: t.name,
description: t.description,
connection: t._meta.connectionTitle,
// Include UI resource URI if the tool has an associated MCP App
...(meta?.["ui/resourceUri"]
? { uiResourceUri: meta["ui/resourceUri"] as string }
: {}),
};
}),
totalAvailable: filteredTools.length,
});
},
Expand Down
22 changes: 20 additions & 2 deletions apps/mesh/src/aggregator/tool-aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class ToolAggregator {

allTools.push({
...tool,
_meta: { connectionId, connectionTitle },
_meta: { ...tool._meta, connectionId, connectionTitle },
});
mappings.set(tool.name, { connectionId, originalName: tool.name });
}
Expand Down Expand Up @@ -152,7 +152,25 @@ export class ToolAggregator {
arguments: args,
});

return result as CallToolResult;
// Inject connectionId into the result's _meta so the frontend knows
// which connection to use for reading associated resources (e.g., MCP Apps)
const resultWithMeta = result as CallToolResult & {
_meta?: Record<string, unknown>;
};
if (resultWithMeta._meta) {
resultWithMeta._meta = {
...resultWithMeta._meta,
connectionId: mapping.connectionId,
};
} else if (
resultWithMeta._meta === undefined &&
"ui/resourceUri" in (result as Record<string, unknown>)
) {
// If _meta doesn't exist but there's a ui/resourceUri at root level (shouldn't happen but be safe)
resultWithMeta._meta = { connectionId: mapping.connectionId };
}

return resultWithMeta as CallToolResult;
};

// Apply the strategy to transform tools
Expand Down
17 changes: 16 additions & 1 deletion apps/mesh/src/api/routes/decopilot/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,25 @@ export async function toolsFromMCP(
};
}
if ("structuredContent" in output) {
return {
// Include _meta if present in the output
const result: { type: "json"; value: JSONValue } = {
type: "json",
value: output.structuredContent as JSONValue,
};
if ("_meta" in output && output._meta) {
(result.value as Record<string, unknown>)._meta = output._meta;
}
return result;
}
// For content type, wrap in an object that includes _meta
if ("_meta" in output && output._meta) {
return {
type: "json",
value: {
content: output.content,
_meta: output._meta,
} as JSONValue,
};
}
return { type: "content", value: output.content as any };
},
Expand Down
76 changes: 73 additions & 3 deletions apps/mesh/src/api/utils/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ export interface ToolDefinition {
};
}

/**
* Resource definition for MCP Apps UI resources
*/
export interface ResourceDefinition {
uri: string;
name: string;
description?: string;
mimeType: string;
content: string;
}

/**
* Middleware for intercepting call tool requests
* Wraps tool execution, allowing pre and post processing
Expand Down Expand Up @@ -122,6 +133,7 @@ export interface McpServerConfig {
class McpServerBuilder {
private config: McpServerConfig;
private tools: ToolDefinition[] = [];
private resources: ResourceDefinition[] = [];
private callToolMiddlewares: CallToolMiddleware[] = [];
// Cache JSON Schema conversions to avoid repeated z.toJSONSchema calls
// which accumulate in Zod 4's __zod_globalRegistry and cause memory leaks
Expand All @@ -133,7 +145,7 @@ class McpServerBuilder {
constructor(config: McpServerConfig) {
this.config = {
...config,
capabilities: config.capabilities ?? { tools: {} },
capabilities: config.capabilities ?? { tools: {}, resources: {} },
};
}

Expand Down Expand Up @@ -170,6 +182,22 @@ class McpServerBuilder {
return this;
}

/**
* Add a resource to the server
*/
withResource(resource: ResourceDefinition): this {
this.resources.push(resource);
return this;
}

/**
* Add multiple resources to the server
*/
withResources(resources: ResourceDefinition[]): this {
this.resources.push(...resources);
return this;
}

/**
* Add middleware for call tool requests
* Middleware runs AFTER tool execution
Expand Down Expand Up @@ -204,14 +232,35 @@ class McpServerBuilder {
): Promise<CallToolResult> => {
try {
const result = await tool.handler(args);

// Check if result contains _meta (for MCP Apps UI resources)
const resultObj = result as Record<string, unknown> | null;
const hasMeta =
resultObj &&
typeof resultObj === "object" &&
"_meta" in resultObj;
const meta = hasMeta
? (resultObj._meta as Record<string, unknown>)
: undefined;

// Remove _meta from structuredContent to keep it clean
const structuredContent = hasMeta
? Object.fromEntries(
Object.entries(resultObj).filter(([k]) => k !== "_meta"),
)
: resultObj;

return {
content: [
{
type: "text" as const,
text: JSON.stringify(result),
text: JSON.stringify(structuredContent),
},
],
structuredContent: result as { [x: string]: unknown } | undefined,
structuredContent: structuredContent as
| { [x: string]: unknown }
| undefined,
...(meta ? { _meta: meta } : {}),
};
} catch (error) {
const err = error as Error;
Expand Down Expand Up @@ -268,6 +317,27 @@ class McpServerBuilder {
);
}

// Register resources (for MCP Apps UI)
for (const resource of this.resources) {
server.resource(
resource.name,
resource.uri,
{
description: resource.description,
mimeType: resource.mimeType,
},
async () => ({
contents: [
{
uri: resource.uri,
mimeType: resource.mimeType,
text: resource.content,
},
],
}),
);
}

return server;
};

Expand Down
22 changes: 22 additions & 0 deletions apps/mesh/src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,25 @@

/** MCP Mesh metadata key in tool _meta */
export const MCP_MESH_KEY = "mcp.mesh";

/**
* MCP Apps feature flag
*
* When enabled, Mesh will render interactive UIs for tools
* that declare UI resources via _meta["ui/resourceUri"].
*/
export const MCP_APPS_ENABLED = true;

/**
* MCP Apps configuration
*/
export const MCP_APPS_CONFIG = {
/** Minimum height for MCP App iframes in pixels */
minHeight: 100,
/** Maximum height for MCP App iframes in pixels */
maxHeight: 600,
/** Default height for MCP App iframes in pixels */
defaultHeight: 300,
/** Whether to show raw JSON output alongside MCP Apps in developer mode */
showRawOutputInDevMode: true,
} as const;
Loading
Loading