Skip to content

Commit 5454b73

Browse files
fix: passthrough tool call when catalog fails to load
When a package's catalog status is 'error' (e.g., the upstream server doesn't respond to tools/list), attempt to forward the callTool request directly instead of hard-failing with PACKAGE_UNAVAILABLE. This handles MCP servers where tools/list is broken but tools/call works (e.g., Figma Desktop MCP). Schema validation is skipped in this path — the upstream server validates arguments itself. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
1 parent 8fc3fbd commit 5454b73

File tree

2 files changed

+58
-6
lines changed

2 files changed

+58
-6
lines changed

src/handlers/useTool.ts

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,64 @@ export async function handleUseTool(
101101
data: { package_id, status: packageStatus },
102102
};
103103
}
104-
if (packageStatus === "error") {
104+
// When the catalog failed to load tools (e.g., server doesn't respond to tools/list),
105+
// attempt a passthrough call rather than hard-failing. Some MCP servers support
106+
// tools/call without supporting tools/list (e.g., Figma Desktop MCP).
107+
// Skip schema validation since we don't have the schema.
108+
const catalogDegraded = packageStatus === "error";
109+
if (catalogDegraded) {
105110
const reason = catalog.getPackageError(package_id) || "See logs for details";
106-
throw {
107-
code: ERROR_CODES.PACKAGE_UNAVAILABLE,
108-
message: `Package '${package_id}' is unavailable: ${reason}`,
109-
data: { package_id, status: packageStatus },
110-
};
111+
logger.warn("Catalog unavailable, attempting passthrough tool call", {
112+
package_id,
113+
tool_id,
114+
catalog_error: reason,
115+
});
116+
117+
if (dry_run) {
118+
throw {
119+
code: ERROR_CODES.PACKAGE_UNAVAILABLE,
120+
message: `Package '${package_id}' catalog is unavailable (${reason}). Cannot dry-run without schema.`,
121+
data: { package_id, status: packageStatus },
122+
};
123+
}
124+
125+
const startTime = Date.now();
126+
try {
127+
const client = await registry.getClient(package_id);
128+
const toolResult = await client.callTool(tool_id, args);
129+
registry.notifyActivity(package_id);
130+
const duration = Date.now() - startTime;
131+
132+
logger.info("Passthrough tool call succeeded despite catalog error", {
133+
package_id,
134+
tool_id,
135+
duration_ms: duration,
136+
});
137+
138+
const result: UseToolOutput = {
139+
package_id,
140+
tool_id,
141+
args_used: args,
142+
result: toolResult,
143+
telemetry: { duration_ms: duration, status: "ok", passthrough: true },
144+
};
145+
146+
let outputJson = JSON.stringify(result, null, 2);
147+
if (max_output_chars && outputJson.length > max_output_chars) {
148+
outputJson = outputJson.slice(0, max_output_chars) +
149+
`\n\n[OUTPUT TRUNCATED: ${max_output_chars.toLocaleString()} of ${outputJson.length.toLocaleString()} chars]`;
150+
}
151+
return { content: [{ type: "text", text: outputJson }], isError: false };
152+
} catch (error) {
153+
registry.notifyActivity(package_id);
154+
const duration = Date.now() - startTime;
155+
const errorMessage = error instanceof Error ? error.message : String(error);
156+
throw {
157+
code: ERROR_CODES.PACKAGE_UNAVAILABLE,
158+
message: `Package '${package_id}' is unavailable: catalog error (${reason}), passthrough also failed (${errorMessage})`,
159+
data: { package_id, tool_id, status: packageStatus, duration_ms: duration },
160+
};
161+
}
111162
}
112163

113164
const schema = await catalog.getToolSchema(package_id, tool_id);

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export interface UseToolOutput {
173173
output_chars?: number;
174174
output_truncated?: boolean;
175175
original_output_chars?: number;
176+
passthrough?: boolean;
176177
};
177178
}
178179

0 commit comments

Comments
 (0)