Skip to content

Commit 738802c

Browse files
committed
Add fixture validation script and update README for testing instructions. Introduce new test command for validating WebMCP fixtures and enhance documentation on using fixtures.
1 parent 9d56d76 commit 738802c

11 files changed

Lines changed: 509 additions & 289 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { getAgentActionUtilsRecord } from '../../../shared/AgentUtils'
2+
import { AgentActionUtil } from '../../../shared/actions/AgentActionUtil'
3+
import { AgentAction } from '../../../shared/types/AgentAction'
4+
import { TldrawAgent } from '../../agent/TldrawAgent'
5+
import { ACTION_PURPOSES, ACTION_TOOL_NAMES, UNTRUSTED_ACTION_TOOL_NAMES, type ToolName } from '../constants'
6+
import { createActionInputSchema, executeAction } from '../execute/executeAction'
7+
import type { CatalogEntry } from '../types'
8+
9+
export function registerActionTools(agent: TldrawAgent, catalog: Map<ToolName, CatalogEntry>) {
10+
const actionUtils = getAgentActionUtilsRecord(agent)
11+
12+
for (const name of ACTION_TOOL_NAMES) {
13+
const util = actionUtils[name] as AgentActionUtil<AgentAction>
14+
const actionSchema = util.getSchema()
15+
if (!actionSchema) {
16+
console.warn(`WebMCP action tool "${name}" has no schema and was not registered.`)
17+
continue
18+
}
19+
20+
catalog.set(name, {
21+
name,
22+
purpose: ACTION_PURPOSES[name],
23+
inputSchema: createActionInputSchema(name, actionSchema),
24+
execute: (input, context) => executeAction(agent, util, actionSchema, name, input, context),
25+
readOnly: false,
26+
untrustedContent: UNTRUSTED_ACTION_TOOL_NAMES.has(name),
27+
})
28+
}
29+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
createTLStore,
3+
defaultBindingUtils,
4+
defaultShapeUtils,
5+
defaultTools,
6+
Editor,
7+
} from 'tldraw'
8+
import { describe, expect, it } from 'vitest'
9+
import { TldrawAgent } from '../../agent/TldrawAgent'
10+
import { ALL_TOOL_NAMES, DISCOVERY_TOOL_NAMES } from '../toolNames'
11+
import { createCatalog } from './createCatalog'
12+
13+
function createTestAgent() {
14+
const container = document.createElement('div')
15+
document.body.appendChild(container)
16+
const store = createTLStore({ shapeUtils: defaultShapeUtils })
17+
const editor = new Editor({
18+
store,
19+
shapeUtils: defaultShapeUtils,
20+
bindingUtils: defaultBindingUtils,
21+
tools: defaultTools,
22+
getContainer: () => container,
23+
})
24+
return new TldrawAgent({ editor, id: 'test-agent', onError: () => {} })
25+
}
26+
27+
describe('createCatalog', () => {
28+
it('registers every tool name', () => {
29+
const catalog = createCatalog(createTestAgent())
30+
31+
expect(catalog.size).toBe(ALL_TOOL_NAMES.length)
32+
for (const name of ALL_TOOL_NAMES) {
33+
expect(catalog.has(name), `missing catalog entry for ${name}`).toBe(true)
34+
}
35+
})
36+
37+
it('marks discovery tools read-only', () => {
38+
const catalog = createCatalog(createTestAgent())
39+
40+
for (const name of DISCOVERY_TOOL_NAMES) {
41+
expect(catalog.get(name)?.readOnly).toBe(true)
42+
}
43+
})
44+
45+
it('requires confirm in destructive action schemas', () => {
46+
const catalog = createCatalog(createTestAgent())
47+
48+
for (const name of ['delete', 'clear'] as const) {
49+
const schema = catalog.get(name)?.inputSchema
50+
expect(schema?.properties?.confirm).toMatchObject({ type: 'boolean' })
51+
expect(schema?.required ?? []).toContain('confirm')
52+
}
53+
})
54+
55+
it('marks getInspiration as untrusted content', () => {
56+
const catalog = createCatalog(createTestAgent())
57+
expect(catalog.get('getInspiration')?.untrustedContent).toBe(true)
58+
})
59+
})
Lines changed: 8 additions & 284 deletions
Original file line numberDiff line numberDiff line change
@@ -1,291 +1,15 @@
1-
import { getAgentActionUtilsRecord } from '../../../shared/AgentUtils'
2-
import { AgentActionUtil } from '../../../shared/actions/AgentActionUtil'
3-
import { AgentAction } from '../../../shared/types/AgentAction'
4-
import { zoomOutCanvas } from '../../../shared/zoomOutCanvas'
1+
// Plan 005-B: factory helper (defineCatalogTool) + domain sub-builders.
52
import { TldrawAgent } from '../../agent/TldrawAgent'
6-
import { searchCommonsImages } from '../../commons/commonsImages'
7-
import { insertCommonsImage } from '../../commons/insertCommonsImage'
8-
import { previewMermaidDiagram } from '../../mermaid/mermaidDiagram'
9-
import {
10-
ACTION_PURPOSES,
11-
ACTION_TOOL_NAMES,
12-
CAMERA_PURPOSES,
13-
COMMONS_PURPOSES,
14-
EMBED_PURPOSES,
15-
GRID_PURPOSES,
16-
MERMAID_PURPOSES,
17-
NEW_DRAW_PAGE_WORKFLOW,
18-
PAGE_PURPOSES,
19-
UNTRUSTED_ACTION_TOOL_NAMES,
20-
type ToolName,
21-
} from '../constants'
22-
import { inspectEmbeds } from '../embedTools'
23-
import { createActionInputSchema, executeAction } from '../execute/executeAction'
24-
import { arrangeGrid } from '../gridTools'
25-
import { createCanvasPage, inspectCanvasPages } from '../pageTools'
26-
import {
27-
ADD_COMMONS_IMAGE_INPUT_SCHEMA,
28-
ARRANGE_GRID_INPUT_SCHEMA,
29-
AddCommonsImageInput,
30-
ArrangeGridInput,
31-
CREATE_MERMAID_DIAGRAM_INPUT_SCHEMA,
32-
CREATE_PAGE_INPUT_SCHEMA,
33-
CreateMermaidDiagramInput,
34-
CreatePageInput,
35-
DESCRIBE_TOOLS_INPUT_SCHEMA,
36-
DescribeToolsInput,
37-
INSPECT_EMBEDS_INPUT_SCHEMA,
38-
INSPECT_PAGES_INPUT_SCHEMA,
39-
InspectEmbedsInput,
40-
InspectPagesInput,
41-
LIST_TOOLS_INPUT_SCHEMA,
42-
SEARCH_COMMONS_IMAGES_INPUT_SCHEMA,
43-
SearchCommonsImagesInput,
44-
ZOOM_OUT_INPUT_SCHEMA,
45-
ZoomOutInput,
46-
} from '../schemas/standaloneSchemas'
47-
import { invalidArgumentsResult, isAbortError, toolErrorResult } from '../toolResults'
3+
import type { ToolName } from '../constants'
484
import type { CatalogEntry } from '../types'
5+
import { registerActionTools } from './actionTools'
6+
import { registerDiscoveryTools } from './discoveryTools'
7+
import { registerStandaloneTools } from './standaloneTools'
498

509
export function createCatalog(agent: TldrawAgent) {
51-
const actionUtils = getAgentActionUtilsRecord(agent)
5210
const catalog = new Map<ToolName, CatalogEntry>()
53-
54-
for (const name of ACTION_TOOL_NAMES) {
55-
const util = actionUtils[name] as AgentActionUtil<AgentAction>
56-
const actionSchema = util.getSchema()
57-
if (!actionSchema) {
58-
console.warn(`WebMCP action tool "${name}" has no schema and was not registered.`)
59-
continue
60-
}
61-
62-
catalog.set(name, {
63-
name,
64-
purpose: ACTION_PURPOSES[name],
65-
inputSchema: createActionInputSchema(name, actionSchema),
66-
execute: (input, context) => executeAction(agent, util, actionSchema, name, input, context),
67-
readOnly: false,
68-
untrustedContent: UNTRUSTED_ACTION_TOOL_NAMES.has(name),
69-
})
70-
}
71-
72-
catalog.set('search_commons_images', {
73-
name: 'search_commons_images',
74-
purpose: COMMONS_PURPOSES.search_commons_images,
75-
inputSchema: SEARCH_COMMONS_IMAGES_INPUT_SCHEMA,
76-
execute: async (input, context) => {
77-
const parsed = SearchCommonsImagesInput.safeParse(input)
78-
if (!parsed.success) return invalidArgumentsResult('search_commons_images', parsed.error)
79-
80-
try {
81-
const images = await searchCommonsImages(parsed.data.query, context?.signal)
82-
return {
83-
ok: true,
84-
tool: 'search_commons_images',
85-
query: parsed.data.query,
86-
count: images.length,
87-
images,
88-
}
89-
} catch (error) {
90-
return toolErrorResult('search_commons_images', error)
91-
}
92-
},
93-
readOnly: true,
94-
untrustedContent: true,
95-
})
96-
97-
catalog.set('add_commons_image', {
98-
name: 'add_commons_image',
99-
purpose: COMMONS_PURPOSES.add_commons_image,
100-
inputSchema: ADD_COMMONS_IMAGE_INPUT_SCHEMA,
101-
execute: async (input, context) => {
102-
const parsed = AddCommonsImageInput.safeParse(input)
103-
if (!parsed.success) return invalidArgumentsResult('add_commons_image', parsed.error)
104-
105-
try {
106-
const result = await insertCommonsImage(agent.editor, parsed.data, context?.signal)
107-
return { ok: true, tool: 'add_commons_image', ...result }
108-
} catch (error) {
109-
return toolErrorResult('add_commons_image', error)
110-
}
111-
},
112-
readOnly: false,
113-
untrustedContent: true,
114-
})
115-
116-
catalog.set('create_mermaid_diagram', {
117-
name: 'create_mermaid_diagram',
118-
purpose: MERMAID_PURPOSES.create_mermaid_diagram,
119-
inputSchema: CREATE_MERMAID_DIAGRAM_INPUT_SCHEMA,
120-
execute: async (input, context) => {
121-
if (context?.signal?.aborted) {
122-
return { ok: false, tool: 'create_mermaid_diagram', error: 'Tool call was aborted.' }
123-
}
124-
125-
const parsed = CreateMermaidDiagramInput.safeParse(input)
126-
if (!parsed.success) return invalidArgumentsResult('create_mermaid_diagram', parsed.error)
127-
128-
try {
129-
const result = await previewMermaidDiagram(agent.editor, parsed.data, context?.signal)
130-
return { ok: true, tool: 'create_mermaid_diagram', ...result }
131-
} catch (error) {
132-
if (isAbortError(error)) {
133-
return { ok: false, tool: 'create_mermaid_diagram', error: 'Tool call was aborted.' }
134-
}
135-
return toolErrorResult('create_mermaid_diagram', error)
136-
}
137-
},
138-
readOnly: false,
139-
untrustedContent: false,
140-
})
141-
142-
catalog.set('create_page', {
143-
name: 'create_page',
144-
purpose: PAGE_PURPOSES.create_page,
145-
inputSchema: CREATE_PAGE_INPUT_SCHEMA,
146-
execute: (input, context) => {
147-
if (context?.signal?.aborted) {
148-
return { ok: false, tool: 'create_page', error: 'Tool call was aborted.' }
149-
}
150-
151-
const parsed = CreatePageInput.safeParse(input ?? {})
152-
if (!parsed.success) return invalidArgumentsResult('create_page', parsed.error)
153-
154-
try {
155-
return { ok: true, tool: 'create_page', ...createCanvasPage(agent.editor) }
156-
} catch (error) {
157-
return toolErrorResult('create_page', error)
158-
}
159-
},
160-
readOnly: false,
161-
untrustedContent: false,
162-
})
163-
164-
catalog.set('inspect_pages', {
165-
name: 'inspect_pages',
166-
purpose: PAGE_PURPOSES.inspect_pages,
167-
inputSchema: INSPECT_PAGES_INPUT_SCHEMA,
168-
execute: (input, context) => {
169-
if (context?.signal?.aborted) {
170-
return { ok: false, tool: 'inspect_pages', error: 'Tool call was aborted.' }
171-
}
172-
173-
const parsed = InspectPagesInput.safeParse(input ?? {})
174-
if (!parsed.success) return invalidArgumentsResult('inspect_pages', parsed.error)
175-
176-
const totalPages = agent.editor.getPages().length
177-
if (parsed.data.pageNumber !== undefined && parsed.data.pageNumber > totalPages) {
178-
return {
179-
ok: false,
180-
tool: 'inspect_pages',
181-
error: `Page number ${parsed.data.pageNumber} does not exist.`,
182-
totalPages,
183-
}
184-
}
185-
186-
return {
187-
ok: true,
188-
tool: 'inspect_pages',
189-
...inspectCanvasPages(agent.editor, parsed.data.pageNumber),
190-
}
191-
},
192-
readOnly: true,
193-
untrustedContent: false,
194-
})
195-
196-
catalog.set('inspect_embeds', {
197-
name: 'inspect_embeds',
198-
purpose: EMBED_PURPOSES.inspect_embeds,
199-
inputSchema: INSPECT_EMBEDS_INPUT_SCHEMA,
200-
execute: (input, context) => {
201-
if (context?.signal?.aborted) {
202-
return { ok: false, tool: 'inspect_embeds', error: 'Tool call was aborted.' }
203-
}
204-
const parsed = InspectEmbedsInput.safeParse(input ?? {})
205-
if (!parsed.success) return invalidArgumentsResult('inspect_embeds', parsed.error)
206-
try {
207-
return { ok: true, tool: 'inspect_embeds', ...inspectEmbeds(agent.editor, parsed.data) }
208-
} catch (error) {
209-
return toolErrorResult('inspect_embeds', error)
210-
}
211-
},
212-
readOnly: true,
213-
untrustedContent: true,
214-
})
215-
216-
catalog.set('zoom_out', {
217-
name: 'zoom_out',
218-
purpose: CAMERA_PURPOSES.zoom_out,
219-
inputSchema: ZOOM_OUT_INPUT_SCHEMA,
220-
execute: (input, context) => {
221-
if (context?.signal?.aborted) {
222-
return { ok: false, tool: 'zoom_out', error: 'Tool call was aborted.' }
223-
}
224-
const parsed = ZoomOutInput.safeParse(input ?? {})
225-
if (!parsed.success) return invalidArgumentsResult('zoom_out', parsed.error)
226-
try {
227-
return { ok: true, tool: 'zoom_out', ...zoomOutCanvas(agent.editor, parsed.data) }
228-
} catch (error) {
229-
return toolErrorResult('zoom_out', error)
230-
}
231-
},
232-
readOnly: false,
233-
untrustedContent: false,
234-
})
235-
236-
catalog.set('arrange_grid', {
237-
name: 'arrange_grid',
238-
purpose: GRID_PURPOSES.arrange_grid,
239-
inputSchema: ARRANGE_GRID_INPUT_SCHEMA,
240-
execute: (input, context) => {
241-
if (context?.signal?.aborted) {
242-
return { ok: false, tool: 'arrange_grid', error: 'Tool call was aborted.' }
243-
}
244-
const parsed = ArrangeGridInput.safeParse(input)
245-
if (!parsed.success) return invalidArgumentsResult('arrange_grid', parsed.error)
246-
try {
247-
return { ok: true, tool: 'arrange_grid', ...arrangeGrid(agent.editor, parsed.data) }
248-
} catch (error) {
249-
return toolErrorResult('arrange_grid', error)
250-
}
251-
},
252-
readOnly: false,
253-
untrustedContent: false,
254-
})
255-
256-
catalog.set('list_tools', {
257-
name: 'list_tools',
258-
purpose:
259-
'List every registered draw-app tool with a one-line summary and the required new-drawing page workflow; call this first to choose tools.',
260-
inputSchema: LIST_TOOLS_INPUT_SCHEMA,
261-
execute: () => ({
262-
workflow: NEW_DRAW_PAGE_WORKFLOW,
263-
tools: [...catalog.values()].map(({ name, purpose }) => ({ name, purpose })),
264-
}),
265-
readOnly: true,
266-
untrustedContent: false,
267-
})
268-
269-
catalog.set('describe_tools', {
270-
name: 'describe_tools',
271-
purpose: 'Return full argument schemas for 1–10 unique tools before calling those compactly registered tools.',
272-
inputSchema: DESCRIBE_TOOLS_INPUT_SCHEMA,
273-
execute: (input) => {
274-
const parsed = DescribeToolsInput.safeParse(input)
275-
if (!parsed.success) return invalidArgumentsResult('describe_tools', parsed.error)
276-
277-
return {
278-
tools: parsed.data.names.map((name) => {
279-
const entry = catalog.get(name)
280-
return entry
281-
? { name: entry.name, purpose: entry.purpose, inputSchema: entry.inputSchema }
282-
: { name, error: 'Tool is not registered.' }
283-
}),
284-
}
285-
},
286-
readOnly: true,
287-
untrustedContent: false,
288-
})
289-
11+
registerActionTools(agent, catalog)
12+
registerStandaloneTools(agent, catalog)
13+
registerDiscoveryTools(catalog)
29014
return catalog
29115
}

0 commit comments

Comments
 (0)