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
74 changes: 36 additions & 38 deletions server/routes/mcp/export.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,57 @@
import { Hono } from "hono";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import "../../types/hono"; // Type extensions

const exporter = new Hono();

// POST /export/server — export all server info as JSON
exporter.post("/server", async (c) => {
try {
const { serverId } = await c.req.json();
const { serverId } = (await c.req.json()) as { serverId?: string };
if (!serverId) {
return c.json({ error: "serverId is required" }, 400);
}

const mcp = c.mcpJamClientManager;
const status = mcp.getConnectionStatus(serverId);
if (status !== "connected") {
return c.json({ error: `Server '${serverId}' is not connected` }, 400);
}
const mcp = c.mcpClientManager;

// Tools
const flattenedTools = await mcp.getToolsetsForServer(serverId);
const tools: Array<{
name: string;
description?: string;
inputSchema: any;
outputSchema?: any;
}> = [];
let toolsResult: Awaited<ReturnType<typeof mcp.listTools>>;
let resourcesResult: Awaited<ReturnType<typeof mcp.listResources>>;
let promptsResult: Awaited<ReturnType<typeof mcp.listPrompts>>;

for (const [name, tool] of Object.entries(flattenedTools)) {
let inputSchema = (tool as any).inputSchema;
try {
inputSchema = zodToJsonSchema(inputSchema as z.ZodType<any>);
} catch {}
tools.push({
name,
description: (tool as any).description,
inputSchema,
outputSchema: (tool as any).outputSchema,
});
try {
[toolsResult, resourcesResult, promptsResult] = await Promise.all([
mcp.listTools(serverId),
mcp.listResources(serverId),
mcp.listPrompts(serverId),
]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (
message.includes("not connected") ||
message.includes("Unknown MCP server")
) {
return c.json({ error: `Server '${serverId}' is not connected` }, 400);
}
throw error;
}

// Resources
const resources = mcp.getResourcesForServer(serverId).map((r) => ({
uri: r.uri,
name: r.name,
description: r.description,
mimeType: r.mimeType,
const tools = toolsResult.tools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
outputSchema: tool.outputSchema,
}));

const resources = resourcesResult.resources.map((resource) => ({
uri: resource.uri,
name: resource.name,
description: resource.description,
mimeType: resource.mimeType,
}));

// Prompts
const prompts = mcp.getPromptsForServer(serverId).map((p) => ({
name: p.name,
description: p.description,
arguments: p.arguments,
const prompts = promptsResult.prompts.map((prompt) => ({
name: prompt.name,
description: prompt.description,
arguments: prompt.arguments,
}));

return c.json({
Expand Down
95 changes: 82 additions & 13 deletions shared/mcp-client-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,20 +248,28 @@ export class MCPClientManager {
) {
await this.ensureConnected(serverId);
const client = this.getClientById(serverId);
const result = await client.listTools(
params,
this.withTimeout(serverId, options),
);
try {
const result = await client.listTools(
params,
this.withTimeout(serverId, options),
);

const metadataMap = new Map<string, any>();
for (const tool of result.tools) {
if (tool._meta) {
metadataMap.set(tool.name, tool._meta);
const metadataMap = new Map<string, any>();
for (const tool of result.tools) {
if (tool._meta) {
metadataMap.set(tool.name, tool._meta);
}
}
}
this.toolsMetadataCache.set(serverId, metadataMap);
this.toolsMetadataCache.set(serverId, metadataMap);

return result;
return result;
} catch (error) {
if (this.isMethodUnavailableError(error, "tools/list")) {
this.toolsMetadataCache.set(serverId, new Map());
return { tools: [] } as Awaited<ReturnType<Client["listTools"]>>;
}
throw error;
}
}

async getTools(serverIds?: string[]): Promise<ListToolsResult> {
Expand Down Expand Up @@ -333,7 +341,19 @@ export class MCPClientManager {
) {
await this.ensureConnected(serverId);
const client = this.getClientById(serverId);
return client.listResources(params, this.withTimeout(serverId, options));
try {
return await client.listResources(
params,
this.withTimeout(serverId, options),
);
} catch (error) {
if (this.isMethodUnavailableError(error, "resources/list")) {
return {
resources: [],
} as Awaited<ReturnType<Client["listResources"]>>;
}
throw error;
}
}

async readResource(
Expand Down Expand Up @@ -392,7 +412,19 @@ export class MCPClientManager {
) {
await this.ensureConnected(serverId);
const client = this.getClientById(serverId);
return client.listPrompts(params, this.withTimeout(serverId, options));
try {
return await client.listPrompts(
params,
this.withTimeout(serverId, options),
);
} catch (error) {
if (this.isMethodUnavailableError(error, "prompts/list")) {
return {
prompts: [],
} as Awaited<ReturnType<Client["listPrompts"]>>;
}
throw error;
}
}

async getPrompt(
Expand Down Expand Up @@ -672,6 +704,43 @@ export class MCPClientManager {
}
}

private isMethodUnavailableError(error: unknown, method: string): boolean {
if (!(error instanceof Error)) {
return false;
}
const message = error.message.toLowerCase();
const methodTokens = new Set<string>();
const pushToken = (token: string) => {
if (token) {
methodTokens.add(token.toLowerCase());
}
};

pushToken(method);
for (const part of method.split(/[\/:._-]/)) {
pushToken(part);
}
const indicators = [
"method not found",
"not implemented",
"unsupported",
"does not support",
"unimplemented",
];
const indicatorMatch = indicators.some((indicator) =>
message.includes(indicator),
);
if (!indicatorMatch) {
return false;
}

if (Array.from(methodTokens).some((token) => message.includes(token))) {
return true;
}

return true;
}

private getTimeout(config: MCPServerConfig): number {
return config.timeout ?? this.defaultTimeout;
}
Expand Down