Skip to content

Commit 8cd48d5

Browse files
cameroncookeclaude
andcommitted
fix(mcp): Apply manifest next-step templating in MCP runtime
Run MCP tool responses through the same template/nextStepParams post-processing used by CLI so migrated tools continue emitting next-step suggestions for LLM clients. Also remove the dead arch parameter from get_sim_app_path now that the schema is simulator-only. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2b9ad85 commit 8cd48d5

3 files changed

Lines changed: 47 additions & 7 deletions

File tree

src/mcp/tools/simulator/get_sim_app_path.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ const baseGetSimulatorAppPathSchema = z.object({
5656
.boolean()
5757
.optional()
5858
.describe('Whether to use the latest OS version for the named simulator'),
59-
arch: z.string().optional().describe('Optional architecture'),
6059
});
6160

6261
// Add XOR validation with preprocessing
@@ -201,7 +200,6 @@ const publicSchemaObject = baseGetSimulatorAppPathSchema.omit({
201200
simulatorName: true,
202201
configuration: true,
203202
useLatestOS: true,
204-
arch: true,
205203
} as const);
206204

207205
export const schema = getSessionAwareToolSchemaShape({

src/runtime/tool-invoker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ function normalizeNextSteps(
191191
};
192192
}
193193

194-
function postProcessToolResponse(params: {
194+
export function postProcessToolResponse(params: {
195195
tool: ToolDefinition;
196196
response: ToolResponse;
197197
args: Record<string, unknown>;

src/utils/tool-registry.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { type RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
22
import { server } from '../server/server-state.ts';
33
import type { ToolResponse } from '../types/common.ts';
4+
import type { ToolCatalog, ToolDefinition } from '../runtime/types.ts';
45
import { log } from './logger.ts';
56
import { processToolResponse } from './responses/index.ts';
67
import { loadManifest } from '../core/manifest/load-manifest.ts';
78
import { importToolModule } from '../core/manifest/import-tool-module.ts';
9+
import { getEffectiveCliName } from '../core/manifest/schema.ts';
10+
import { createToolCatalog } from '../runtime/tool-catalog.ts';
11+
import { postProcessToolResponse } from '../runtime/tool-invoker.ts';
812
import type { PredicateContext } from '../visibility/predicate-types.ts';
913
import { selectWorkflowsForMcp, isToolExposedForRuntime } from '../visibility/exposure.ts';
1014
import { getConfig } from './config-store.ts';
@@ -20,10 +24,13 @@ const registryState: {
2024
enabledWorkflows: Set<string>;
2125
/** Current MCP predicate context (stored for use by manage_workflows) */
2226
currentContext: PredicateContext | null;
27+
/** Catalog of currently registered MCP tools for next-step template resolution */
28+
catalog: ToolCatalog | null;
2329
} = {
2430
tools: new Map<string, RegisteredTool>(),
2531
enabledWorkflows: new Set<string>(),
2632
currentContext: null,
33+
catalog: null,
2734
};
2835

2936
export function getRuntimeRegistration(): RuntimeToolInfo | null {
@@ -79,6 +86,8 @@ export async function applyWorkflowSelectionFromManifest(
7986

8087
const desiredToolNames = new Set<string>();
8188
const desiredWorkflows = new Set<string>();
89+
const catalogTools: ToolDefinition[] = [];
90+
const moduleCache = new Map<string, Awaited<ReturnType<typeof importToolModule>>>();
8291

8392
for (const workflow of selectedWorkflows) {
8493
desiredWorkflows.add(workflow.id);
@@ -95,16 +104,32 @@ export async function applyWorkflowSelectionFromManifest(
95104
const toolName = toolManifest.names.mcp;
96105
desiredToolNames.add(toolName);
97106

98-
if (!registryState.tools.has(toolName)) {
99-
// Import the tool module
100-
let toolModule;
107+
let toolModule = moduleCache.get(toolId);
108+
if (!toolModule) {
101109
try {
102110
toolModule = await importToolModule(toolManifest.module);
111+
moduleCache.set(toolId, toolModule);
103112
} catch (err) {
104113
log('warning', `Failed to import tool module ${toolManifest.module}: ${err}`);
105114
continue;
106115
}
116+
}
107117

118+
catalogTools.push({
119+
id: toolManifest.id,
120+
cliName: getEffectiveCliName(toolManifest),
121+
mcpName: toolName,
122+
workflow: workflow.id,
123+
description: toolManifest.description,
124+
annotations: toolManifest.annotations,
125+
nextStepTemplates: toolManifest.nextSteps,
126+
mcpSchema: toolModule.schema,
127+
cliSchema: toolModule.schema,
128+
stateful: toolManifest.routing?.stateful ?? false,
129+
handler: toolModule.handler as ToolDefinition['handler'],
130+
});
131+
132+
if (!registryState.tools.has(toolName)) {
108133
const registeredTool = server.registerTool(
109134
toolName,
110135
{
@@ -116,14 +141,28 @@ export async function applyWorkflowSelectionFromManifest(
116141
const startedAt = Date.now();
117142
try {
118143
const response = await toolModule.handler(args as Record<string, unknown>);
144+
const catalog = registryState.catalog;
145+
const catalogTool = catalog?.getByMcpName(toolName);
146+
const postProcessedResponse =
147+
catalog && catalogTool
148+
? postProcessToolResponse({
149+
tool: catalogTool,
150+
response: response as ToolResponse,
151+
args: args as Record<string, unknown>,
152+
catalog,
153+
runtime: 'mcp',
154+
})
155+
: (response as ToolResponse);
156+
119157
recordToolInvocationMetric({
120158
toolName,
121159
runtime: 'mcp',
122160
transport: 'direct',
123161
outcome: 'completed',
124162
durationMs: Date.now() - startedAt,
125163
});
126-
return processToolResponse(response as ToolResponse, 'mcp', 'normal');
164+
165+
return processToolResponse(postProcessedResponse, 'mcp', 'normal');
127166
} catch (error) {
128167
recordInternalErrorMetric({
129168
component: 'mcp-tool-registry',
@@ -146,6 +185,8 @@ export async function applyWorkflowSelectionFromManifest(
146185
}
147186
}
148187

188+
registryState.catalog = createToolCatalog(catalogTools);
189+
149190
// Unregister tools no longer in selection
150191
for (const [toolName, registeredTool] of registryState.tools.entries()) {
151192
if (!desiredToolNames.has(toolName)) {
@@ -196,4 +237,5 @@ export function __resetToolRegistryForTests(): void {
196237
registryState.tools.clear();
197238
registryState.enabledWorkflows.clear();
198239
registryState.currentContext = null;
240+
registryState.catalog = null;
199241
}

0 commit comments

Comments
 (0)