Skip to content

Commit ee7f621

Browse files
authored
Merge pull request #595 from w1ne/codex/mcp-catalog-tools
Extract catalog MCP tools
2 parents bbc6f73 + 08b2be6 commit ee7f621

4 files changed

Lines changed: 126 additions & 104 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright (c) 2026 Andrii Shylenko and kernelCAD contributors
3+
import { lookupCookbookTool } from '../tools/lookupCookbook';
4+
import { findPartTool } from '../tools/findPart';
5+
import { fetchPartTool } from '../tools/fetchPart';
6+
import type { ToolRegistryEntry } from './types';
7+
8+
export const catalogToolEntries: ToolRegistryEntry[] = [
9+
{
10+
definition: {
11+
name: 'lookup_cookbook',
12+
description:
13+
'Use this when you need a canonical pattern snippet for a CAD task. ' +
14+
'Search the kernelCAD cookbook for canonical pattern snippets. ' +
15+
'Returns top-k snippets matching the natural-language query, ' +
16+
'ranked by BM25 over title/tags/keywords/trigger. ' +
17+
'Use when you need a canonical pattern for fillet-after-subtract, ' +
18+
'non-overlapping booleans, sketch-to-extrude flows, etc. ' +
19+
'Returns empty if no snippet scores above the relevance floor — ' +
20+
'proceed without cookbook help in that case.',
21+
inputSchema: {
22+
type: 'object',
23+
properties: {
24+
query: {
25+
type: 'string',
26+
description:
27+
'Natural-language description of what you want to do (e.g. "round the rim of a hole", "build an L-bracket").',
28+
},
29+
k: {
30+
type: 'number',
31+
description: 'Max snippets to return. Default 3, max 5.',
32+
default: 3,
33+
},
34+
},
35+
required: ['query'],
36+
},
37+
},
38+
handler: input => lookupCookbookTool(input as unknown as Parameters<typeof lookupCookbookTool>[0]),
39+
},
40+
{
41+
definition: {
42+
name: 'find_part',
43+
description:
44+
'Use this when you need to find a part in the catalog. ' +
45+
'Discover bundled (and optionally remote) part-catalog records by fuzzy query and faceted filters. Tokens AND-combine; cross-facet filters AND-combine. Pass partsBaseUrl (or set KERNELCAD_PARTS_BASE_URL) to enable the remote tier; otherwise results are bundled-only.',
46+
inputSchema: {
47+
type: 'object',
48+
properties: {
49+
query: { type: 'string' },
50+
category: { type: 'string' },
51+
family: { type: 'string' },
52+
standard: { type: 'string' },
53+
tag: { type: 'string' },
54+
limit: { type: 'number' },
55+
source: { type: 'string', enum: ['local', 'remote', 'auto'] },
56+
partsBaseUrl: { type: 'string', description: 'Opt-in remote endpoint; no default value ships with kernelCAD.' },
57+
},
58+
},
59+
},
60+
handler: input => findPartTool(input as Parameters<typeof findPartTool>[0]),
61+
},
62+
{
63+
definition: {
64+
name: 'fetch_part',
65+
description:
66+
'Use this when you need to download a catalog part as a STEP file. ' +
67+
'Resolve an id (or single-match query) to a part record and write its STEP file to the local cache. Bundled ids resolve offline; non-bundled ids require partsBaseUrl (or KERNELCAD_PARTS_BASE_URL). Returns the cache path plus a sha256 fingerprint.',
68+
inputSchema: {
69+
type: 'object',
70+
properties: {
71+
id: { type: 'string' },
72+
query: { type: 'string' },
73+
category: { type: 'string' },
74+
family: { type: 'string' },
75+
standard: { type: 'string' },
76+
partsBaseUrl: { type: 'string', description: 'Opt-in remote endpoint; no default value ships with kernelCAD.' },
77+
},
78+
},
79+
},
80+
handler: input => fetchPartTool(input as Parameters<typeof fetchPartTool>[0]),
81+
},
82+
];

src/agent/mcp/registry/types.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright (c) 2026 Andrii Shylenko and kernelCAD contributors
3+
import type { ToolAnnotations } from '../toolAnnotations';
4+
import type { JSONSchemaObject } from '../toolOutputSchemas';
5+
6+
export interface McpToolDefinition {
7+
name: string;
8+
description: string;
9+
inputSchema: {
10+
type: 'object';
11+
properties: Record<string, unknown>;
12+
required?: string[];
13+
/** Optional JSON Schema conditional blocks (if/then/else) for
14+
* required-by-discriminator fields. */
15+
allOf?: unknown[];
16+
};
17+
/** MCP behavioral hints (readOnly/destructive/openWorld). Required for ChatGPT
18+
* app-directory submission; merged from TOOL_ANNOTATIONS at build time. */
19+
annotations?: ToolAnnotations;
20+
/** MCP structured-output schema (JSON Schema for the tool's return value).
21+
* Merged from TOOL_OUTPUT_SCHEMAS at build time. */
22+
outputSchema?: JSONSchemaObject;
23+
}
24+
25+
export type ToolHandler = (input: Record<string, unknown>) => Promise<unknown>;
26+
27+
export interface ToolRegistryEntry {
28+
definition: McpToolDefinition;
29+
handler: ToolHandler;
30+
}

src/agent/mcp/toolRegistry.publicContract.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
defaultBuildRepairPrompt,
1313
type McpToolDefinition,
1414
} from './toolRegistry';
15+
import { catalogToolEntries } from './registry/catalogTools';
1516

1617
const EXPECTED_TOOL_NAMES = [
1718
'evaluate_script',
@@ -94,6 +95,13 @@ describe('toolRegistry public contract', () => {
9495
expect(serializedPublicTools()).toEqual(fixture);
9596
});
9697

98+
it('composes catalog tools from the catalog registry module', () => {
99+
const names = catalogToolEntries.map(entry => entry.definition.name);
100+
101+
expect(names).toEqual(['lookup_cookbook', 'find_part', 'fetch_part']);
102+
expect(TOOL_REGISTRY.slice(20, 23)).toEqual(catalogToolEntries);
103+
});
104+
97105
it('exports callMcpTool that dispatches by name and returns a result', async () => {
98106
const result = await callMcpTool('lookup_api', {});
99107
expect(result).toBeDefined();

src/agent/mcp/toolRegistry.ts

Lines changed: 6 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ import { evaluateSdfTool } from './tools/evaluateSdf';
2121
import { exportTool } from './tools/export';
2222
import { listApiTool } from './tools/listApi';
2323
import { listDiagnosticCodesTool } from './tools/listDiagnosticCodes';
24-
import { lookupCookbookTool } from './tools/lookupCookbook';
25-
import { findPartTool } from './tools/findPart';
26-
import { fetchPartTool } from './tools/fetchPart';
2724
import { removeFeatureTool } from './tools/removeFeature';
2825
import { designLoopTool } from './tools/designLoop';
2926
import { reviewCadTool } from './tools/reviewCad';
@@ -35,39 +32,16 @@ import { flattenPatternTool } from './tools/flattenPattern';
3532
import { verifyTool } from './tools/verify';
3633
import { inspectTool } from './tools/inspect';
3734
import { queryTool } from './tools/query';
38-
import { TOOL_ANNOTATIONS, type ToolAnnotations } from './toolAnnotations';
39-
import { TOOL_OUTPUT_SCHEMAS, type JSONSchemaObject } from './toolOutputSchemas';
35+
import { TOOL_ANNOTATIONS } from './toolAnnotations';
36+
import { TOOL_OUTPUT_SCHEMAS } from './toolOutputSchemas';
4037
import { captureAnimationTool } from './tools/captureAnimation';
4138
import { renderPreviewTool } from './tools/renderPreview';
39+
import { catalogToolEntries } from './registry/catalogTools';
40+
import type { McpToolDefinition, ToolRegistryEntry } from './registry/types';
4241
export { runClosedLoop } from '../loop/closedLoop.js';
4342
export { buildRepairPrompt } from '../loop/repairPrompt.js';
4443
export * from '../loop/types.js';
45-
46-
export interface McpToolDefinition {
47-
name: string;
48-
description: string;
49-
inputSchema: {
50-
type: 'object';
51-
properties: Record<string, unknown>;
52-
required?: string[];
53-
/** Optional JSON Schema conditional blocks (if/then/else) for
54-
* required-by-discriminator fields. */
55-
allOf?: unknown[];
56-
};
57-
/** MCP behavioral hints (readOnly/destructive/openWorld). Required for ChatGPT
58-
* app-directory submission; merged from TOOL_ANNOTATIONS at build time. */
59-
annotations?: ToolAnnotations;
60-
/** MCP structured-output schema (JSON Schema for the tool's return value).
61-
* Merged from TOOL_OUTPUT_SCHEMAS at build time. */
62-
outputSchema?: JSONSchemaObject;
63-
}
64-
65-
type ToolHandler = (input: Record<string, unknown>) => Promise<unknown>;
66-
67-
interface ToolRegistryEntry {
68-
definition: McpToolDefinition;
69-
handler: ToolHandler;
70-
}
44+
export type { McpToolDefinition } from './registry/types';
7145

7246
/**
7347
* Registry of every MCP tool — pairs each definition with its handler.
@@ -911,79 +885,7 @@ export const TOOL_REGISTRY: ToolRegistryEntry[] = [
911885
},
912886
handler: input => exportTool(input as unknown as Parameters<typeof exportTool>[0]),
913887
},
914-
{
915-
definition: {
916-
name: 'lookup_cookbook',
917-
description:
918-
'Use this when you need a canonical pattern snippet for a CAD task. ' +
919-
'Search the kernelCAD cookbook for canonical pattern snippets. ' +
920-
'Returns top-k snippets matching the natural-language query, ' +
921-
'ranked by BM25 over title/tags/keywords/trigger. ' +
922-
'Use when you need a canonical pattern for fillet-after-subtract, ' +
923-
'non-overlapping booleans, sketch-to-extrude flows, etc. ' +
924-
'Returns empty if no snippet scores above the relevance floor — ' +
925-
'proceed without cookbook help in that case.',
926-
inputSchema: {
927-
type: 'object',
928-
properties: {
929-
query: {
930-
type: 'string',
931-
description:
932-
'Natural-language description of what you want to do (e.g. "round the rim of a hole", "build an L-bracket").',
933-
},
934-
k: {
935-
type: 'number',
936-
description: 'Max snippets to return. Default 3, max 5.',
937-
default: 3,
938-
},
939-
},
940-
required: ['query'],
941-
},
942-
},
943-
handler: input => lookupCookbookTool(input as unknown as Parameters<typeof lookupCookbookTool>[0]),
944-
},
945-
{
946-
definition: {
947-
name: 'find_part',
948-
description:
949-
'Use this when you need to find a part in the catalog. ' +
950-
'Discover bundled (and optionally remote) part-catalog records by fuzzy query and faceted filters. Tokens AND-combine; cross-facet filters AND-combine. Pass partsBaseUrl (or set KERNELCAD_PARTS_BASE_URL) to enable the remote tier; otherwise results are bundled-only.',
951-
inputSchema: {
952-
type: 'object',
953-
properties: {
954-
query: { type: 'string' },
955-
category: { type: 'string' },
956-
family: { type: 'string' },
957-
standard: { type: 'string' },
958-
tag: { type: 'string' },
959-
limit: { type: 'number' },
960-
source: { type: 'string', enum: ['local', 'remote', 'auto'] },
961-
partsBaseUrl: { type: 'string', description: 'Opt-in remote endpoint; no default value ships with kernelCAD.' },
962-
},
963-
},
964-
},
965-
handler: input => findPartTool(input as Parameters<typeof findPartTool>[0]),
966-
},
967-
{
968-
definition: {
969-
name: 'fetch_part',
970-
description:
971-
'Use this when you need to download a catalog part as a STEP file. ' +
972-
'Resolve an id (or single-match query) to a part record and write its STEP file to the local cache. Bundled ids resolve offline; non-bundled ids require partsBaseUrl (or KERNELCAD_PARTS_BASE_URL). Returns the cache path plus a sha256 fingerprint.',
973-
inputSchema: {
974-
type: 'object',
975-
properties: {
976-
id: { type: 'string' },
977-
query: { type: 'string' },
978-
category: { type: 'string' },
979-
family: { type: 'string' },
980-
standard: { type: 'string' },
981-
partsBaseUrl: { type: 'string', description: 'Opt-in remote endpoint; no default value ships with kernelCAD.' },
982-
},
983-
},
984-
},
985-
handler: input => fetchPartTool(input as Parameters<typeof fetchPartTool>[0]),
986-
},
888+
...catalogToolEntries,
987889
{
988890
definition: {
989891
name: 'solve_sketch',

0 commit comments

Comments
 (0)