@open-mercato/ai-assistant 0.7.0 → 0.7.1-develop.7103.1.41ff100d93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -1
- package/dist/modules/ai_assistant/ai-tools/search-pack.js +93 -3
- package/dist/modules/ai_assistant/ai-tools/search-pack.js.map +3 -3
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js +2 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/lib/codemode-tools.js +14 -6
- package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js +10 -2
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
- package/dist/modules/ai_assistant/lib/http-server.js +3 -1
- package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/in-process-client.js +3 -1
- package/dist/modules/ai_assistant/lib/in-process-client.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-client.js +2 -1
- package/dist/modules/ai_assistant/lib/mcp-client.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js +3 -1
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-server.js +3 -1
- package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-tool-annotations.js +18 -0
- package/dist/modules/ai_assistant/lib/mcp-tool-annotations.js.map +7 -0
- package/package.json +8 -7
- package/src/modules/ai_assistant/__tests__/integration/ws-c-tool-pack-coverage.test.ts +5 -0
- package/src/modules/ai_assistant/ai-tools/__tests__/search-pack.test.ts +211 -5
- package/src/modules/ai_assistant/ai-tools/search-pack.ts +110 -4
- package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx +3 -0
- package/src/modules/ai_assistant/lib/__tests__/codemode-tool-annotations.test.ts +58 -0
- package/src/modules/ai_assistant/lib/__tests__/generated-registry-loader.test.ts +16 -0
- package/src/modules/ai_assistant/lib/__tests__/mcp-client.test.ts +30 -0
- package/src/modules/ai_assistant/lib/__tests__/mcp-server-tool-annotations.test.ts +120 -0
- package/src/modules/ai_assistant/lib/__tests__/mcp-tool-annotations.test.ts +57 -0
- package/src/modules/ai_assistant/lib/codemode-tools.ts +21 -7
- package/src/modules/ai_assistant/lib/generated-registry-loader.ts +10 -2
- package/src/modules/ai_assistant/lib/http-server.ts +2 -0
- package/src/modules/ai_assistant/lib/in-process-client.ts +2 -0
- package/src/modules/ai_assistant/lib/mcp-client.ts +1 -0
- package/src/modules/ai_assistant/lib/mcp-dev-server.ts +2 -0
- package/src/modules/ai_assistant/lib/mcp-server.ts +2 -0
- package/src/modules/ai_assistant/lib/mcp-tool-annotations.ts +35 -0
- package/src/modules/ai_assistant/lib/types.ts +11 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
3
|
+
import { createMcpServer } from '../mcp-server'
|
|
4
|
+
import type { AiToolDefinition, McpServerOptions, McpToolDefinition } from '../types'
|
|
5
|
+
|
|
6
|
+
const mockCapturedHandlers = new Map<unknown, (request: unknown) => Promise<unknown>>()
|
|
7
|
+
const mockRegisteredTools = new Map<string, McpToolDefinition>()
|
|
8
|
+
|
|
9
|
+
jest.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
10
|
+
Server: class {
|
|
11
|
+
setRequestHandler(schema: unknown, handler: (request: unknown) => Promise<unknown>) {
|
|
12
|
+
mockCapturedHandlers.set(schema, handler)
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
}))
|
|
16
|
+
jest.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
|
|
17
|
+
StdioServerTransport: class {},
|
|
18
|
+
}))
|
|
19
|
+
jest.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
|
20
|
+
ListToolsRequestSchema: { id: 'tools/list' },
|
|
21
|
+
CallToolRequestSchema: { id: 'tools/call' },
|
|
22
|
+
}))
|
|
23
|
+
jest.mock('../tool-registry', () => ({
|
|
24
|
+
getToolRegistry: () => ({
|
|
25
|
+
getTools: () => mockRegisteredTools,
|
|
26
|
+
listToolNames: () => Array.from(mockRegisteredTools.keys()),
|
|
27
|
+
}),
|
|
28
|
+
}))
|
|
29
|
+
jest.mock('../tool-executor', () => ({ executeTool: jest.fn() }))
|
|
30
|
+
jest.mock('../tool-loader', () => ({
|
|
31
|
+
loadAllModuleTools: jest.fn(),
|
|
32
|
+
indexToolsForSearch: jest.fn(),
|
|
33
|
+
}))
|
|
34
|
+
jest.mock('../auth', () => ({
|
|
35
|
+
authenticateMcpRequest: jest.fn(),
|
|
36
|
+
hasRequiredFeatures: jest.fn(() => true),
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
function registerTool(overrides: Partial<AiToolDefinition> & { name: string }): void {
|
|
40
|
+
mockRegisteredTools.set(overrides.name, {
|
|
41
|
+
description: `${overrides.name} description`,
|
|
42
|
+
inputSchema: z.object({ id: z.string() }),
|
|
43
|
+
handler: async () => ({}),
|
|
44
|
+
...overrides,
|
|
45
|
+
} as McpToolDefinition)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeContainer(): McpServerOptions['container'] {
|
|
49
|
+
return {
|
|
50
|
+
resolve: () => ({ loadAcl: jest.fn() }),
|
|
51
|
+
} as unknown as McpServerOptions['container']
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type ListedTool = {
|
|
55
|
+
name: string
|
|
56
|
+
description: string
|
|
57
|
+
inputSchema: unknown
|
|
58
|
+
annotations?: { readOnlyHint?: boolean; destructiveHint?: boolean }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function listTools(): Promise<ListedTool[]> {
|
|
62
|
+
await createMcpServer({
|
|
63
|
+
config: { name: 'test-mcp', version: '0.0.0' },
|
|
64
|
+
container: makeContainer(),
|
|
65
|
+
allowUnauthenticatedSuperadmin: true,
|
|
66
|
+
})
|
|
67
|
+
const handler = mockCapturedHandlers.get(ListToolsRequestSchema)
|
|
68
|
+
if (!handler) throw new Error('[internal] tools/list handler was not registered')
|
|
69
|
+
const response = (await handler({})) as { tools: ListedTool[] }
|
|
70
|
+
return response.tools
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('issue #5283 — MCP tools/list publishes readOnlyHint annotations', () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
mockRegisteredTools.clear()
|
|
76
|
+
mockCapturedHandlers.clear()
|
|
77
|
+
jest.spyOn(process.stderr, 'write').mockImplementation(() => true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
jest.restoreAllMocks()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('marks read operations as read-only so clients can skip approval', async () => {
|
|
85
|
+
registerTool({ name: 'customers.get_company' })
|
|
86
|
+
registerTool({ name: 'customers.list_people' })
|
|
87
|
+
registerTool({ name: 'search_status', isMutation: false })
|
|
88
|
+
|
|
89
|
+
const tools = await listTools()
|
|
90
|
+
|
|
91
|
+
expect(tools).toHaveLength(3)
|
|
92
|
+
for (const tool of tools) {
|
|
93
|
+
expect(tool.annotations?.readOnlyHint).toBe(true)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('never marks mutating operations as read-only', async () => {
|
|
98
|
+
registerTool({ name: 'catalog.update_product', isMutation: true })
|
|
99
|
+
registerTool({ name: 'customers.manage_deal_comment', isMutation: true, isDestructive: true })
|
|
100
|
+
|
|
101
|
+
const tools = await listTools()
|
|
102
|
+
|
|
103
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]))
|
|
104
|
+
expect(byName.get('catalog.update_product')?.annotations?.readOnlyHint).toBe(false)
|
|
105
|
+
expect(byName.get('customers.manage_deal_comment')?.annotations).toEqual({
|
|
106
|
+
readOnlyHint: false,
|
|
107
|
+
destructiveHint: true,
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('keeps the existing name / description / inputSchema fields intact', async () => {
|
|
112
|
+
registerTool({ name: 'customers.get_company' })
|
|
113
|
+
|
|
114
|
+
const [tool] = await listTools()
|
|
115
|
+
|
|
116
|
+
expect(tool.name).toBe('customers.get_company')
|
|
117
|
+
expect(tool.description).toBe('customers.get_company description')
|
|
118
|
+
expect(tool.inputSchema).toBeDefined()
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { buildMcpToolAnnotations } from '../mcp-tool-annotations'
|
|
3
|
+
import type { AiToolDefinition } from '../types'
|
|
4
|
+
|
|
5
|
+
function makeTool(overrides: Partial<AiToolDefinition> = {}): AiToolDefinition {
|
|
6
|
+
return {
|
|
7
|
+
name: 'customers.get_company',
|
|
8
|
+
description: 'Fetch a single company record.',
|
|
9
|
+
inputSchema: z.object({ id: z.string() }),
|
|
10
|
+
handler: async () => ({}),
|
|
11
|
+
...overrides,
|
|
12
|
+
} as AiToolDefinition
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('buildMcpToolAnnotations', () => {
|
|
16
|
+
it('advertises readOnlyHint for tools that do not declare a mutation', () => {
|
|
17
|
+
expect(buildMcpToolAnnotations(makeTool())).toEqual({ readOnlyHint: true })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('advertises readOnlyHint for tools that explicitly declare isMutation: false', () => {
|
|
21
|
+
expect(buildMcpToolAnnotations(makeTool({ isMutation: false }))).toEqual({ readOnlyHint: true })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('never advertises readOnlyHint for mutating tools', () => {
|
|
25
|
+
expect(buildMcpToolAnnotations(makeTool({ isMutation: true }))).toEqual({ readOnlyHint: false })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('advertises destructiveHint when the mutation declares isDestructive', () => {
|
|
29
|
+
expect(buildMcpToolAnnotations(makeTool({ isMutation: true, isDestructive: true }))).toEqual({
|
|
30
|
+
readOnlyHint: false,
|
|
31
|
+
destructiveHint: true,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('carries an explicit non-destructive declaration through', () => {
|
|
36
|
+
expect(buildMcpToolAnnotations(makeTool({ isMutation: true, isDestructive: false }))).toEqual({
|
|
37
|
+
readOnlyHint: false,
|
|
38
|
+
destructiveHint: false,
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('resolves a per-input isDestructive predicate to true rather than under-warning', () => {
|
|
43
|
+
const annotations = buildMcpToolAnnotations(
|
|
44
|
+
makeTool({ isMutation: true, isDestructive: (input: unknown) => (input as { op: string }).op === 'delete' }),
|
|
45
|
+
)
|
|
46
|
+
expect(annotations).toEqual({ readOnlyHint: false, destructiveHint: true })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('omits destructiveHint when a mutation does not declare isDestructive', () => {
|
|
50
|
+
expect(buildMcpToolAnnotations(makeTool({ isMutation: true }))).not.toHaveProperty('destructiveHint')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('never advertises idempotentHint, which no tool definition carries', () => {
|
|
54
|
+
expect(buildMcpToolAnnotations(makeTool())).not.toHaveProperty('idempotentHint')
|
|
55
|
+
expect(buildMcpToolAnnotations(makeTool({ isMutation: true }))).not.toHaveProperty('idempotentHint')
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -13,7 +13,7 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
|
13
13
|
import { z } from 'zod'
|
|
14
14
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
15
15
|
import { registerMcpTool } from './tool-registry'
|
|
16
|
-
import type { McpToolContext } from './types'
|
|
16
|
+
import type { AiToolDefinition, McpToolContext } from './types'
|
|
17
17
|
import { createSandbox } from './sandbox'
|
|
18
18
|
import { truncateResult } from './truncate'
|
|
19
19
|
import { applyContextScopeToQuery, applyContextScopeToBody } from './scope-injection'
|
|
@@ -560,6 +560,15 @@ export const CODE_MODE_MAX_API_CALLS = 50
|
|
|
560
560
|
/** Maximum mutation (non-GET/HEAD/OPTIONS) api.request() calls allowed per execute() run. */
|
|
561
561
|
export const CODE_MODE_MAX_MUTATION_CALLS = 20
|
|
562
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Register a Code Mode tool through the typed definition so the optional
|
|
565
|
+
* metadata (`isMutation`, `isDestructive`) survives registration — the MCP
|
|
566
|
+
* `tools/list` annotations are derived from those flags.
|
|
567
|
+
*/
|
|
568
|
+
function registerCodeModeTool(tool: AiToolDefinition<{ code: string }>): void {
|
|
569
|
+
registerMcpTool(tool, { moduleId: 'codemode' })
|
|
570
|
+
}
|
|
571
|
+
|
|
563
572
|
/**
|
|
564
573
|
* Load and register the two Code Mode tools.
|
|
565
574
|
* Generates TypeScript type stubs for common endpoints at startup.
|
|
@@ -576,9 +585,10 @@ export async function loadCodeModeTools(): Promise<number> {
|
|
|
576
585
|
* search — Query the OpenAPI spec and entity graph programmatically.
|
|
577
586
|
*/
|
|
578
587
|
function registerSearchTool(): void {
|
|
579
|
-
|
|
588
|
+
registerCodeModeTool(
|
|
580
589
|
{
|
|
581
590
|
name: 'search',
|
|
591
|
+
isMutation: false,
|
|
582
592
|
description: `Query the OpenAPI spec and entity schemas. READ-ONLY, no side effects.
|
|
583
593
|
Globals: spec.findEndpoints(keyword), spec.describeEndpoint(path, method), spec.describeEntity(keyword), spec.paths, spec.entitySchemas.
|
|
584
594
|
Use BEFORE execute to learn endpoint schemas for CREATE/UPDATE. Skip for common paths (companies, people, orders, quotes, products).`,
|
|
@@ -650,8 +660,7 @@ Use BEFORE execute to learn endpoint schemas for CREATE/UPDATE. Skip for common
|
|
|
650
660
|
_memoryContext: memoryContext,
|
|
651
661
|
}
|
|
652
662
|
},
|
|
653
|
-
}
|
|
654
|
-
{ moduleId: 'codemode' }
|
|
663
|
+
}
|
|
655
664
|
)
|
|
656
665
|
}
|
|
657
666
|
|
|
@@ -663,9 +672,15 @@ function registerExecuteTool(commonTypes: string): void {
|
|
|
663
672
|
? `\n\n${commonTypes}`
|
|
664
673
|
: ''
|
|
665
674
|
|
|
666
|
-
|
|
675
|
+
registerCodeModeTool(
|
|
667
676
|
{
|
|
668
677
|
name: 'execute',
|
|
678
|
+
// api.request() reaches every documented endpoint, including POST/PUT/DELETE,
|
|
679
|
+
// so the tool is neither read-only nor guaranteed non-destructive. It is
|
|
680
|
+
// intentionally exempt from prepareMutation: arbitrary sandbox code cannot
|
|
681
|
+
// provide the structured before/after preview that approval flow requires.
|
|
682
|
+
isMutation: true,
|
|
683
|
+
isDestructive: true,
|
|
669
684
|
description: `Make API calls. Returns JSON.
|
|
670
685
|
Globals: api.request({ method, path, query?, body? }) → { success, statusCode, data }, context { tenantId, organizationId, userId }.
|
|
671
686
|
RULES: For FIND/LIST → GET only (1 call). For UPDATE → PUT to collection path with id in BODY. NEVER PUT/POST/DELETE unless user explicitly asked to change data. Before ANY write operation (POST/PUT/DELETE), you MUST use the AskUserQuestion tool to get explicit user confirmation. Do NOT just ask in text — use the tool so execution pauses until the user responds.${typesBlock}`,
|
|
@@ -749,8 +764,7 @@ RULES: For FIND/LIST → GET only (1 call). For UPDATE → PUT to collection pat
|
|
|
749
764
|
_memoryContext: memoryContext,
|
|
750
765
|
}
|
|
751
766
|
},
|
|
752
|
-
}
|
|
753
|
-
{ moduleId: 'codemode' }
|
|
767
|
+
}
|
|
754
768
|
)
|
|
755
769
|
}
|
|
756
770
|
|
|
@@ -121,7 +121,11 @@ export async function compileAndImportGenerated(tsPath: string): Promise<Record<
|
|
|
121
121
|
if (useJestCjsArtifact) {
|
|
122
122
|
return requireFromHere(jsPath) as Record<string, unknown>
|
|
123
123
|
}
|
|
124
|
-
return (await import(
|
|
124
|
+
return (await import(
|
|
125
|
+
/* webpackIgnore: true */
|
|
126
|
+
/* turbopackIgnore: true */
|
|
127
|
+
pathToFileURL(jsPath).href
|
|
128
|
+
)) as Record<string, unknown>
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
function isJestRuntime(): boolean {
|
|
@@ -169,7 +173,11 @@ async function compileAppLocalModuleEntries(
|
|
|
169
173
|
if (specifiers.length === 0) return artifacts
|
|
170
174
|
|
|
171
175
|
const generatedDir = path.join(appRoot, '.mercato', 'generated')
|
|
172
|
-
const { compileAppSourceFile } = await import(
|
|
176
|
+
const { compileAppSourceFile } = await import(
|
|
177
|
+
/* webpackIgnore: true */
|
|
178
|
+
/* turbopackIgnore: true */
|
|
179
|
+
'@open-mercato/shared/lib/bootstrap/dynamicLoader'
|
|
180
|
+
)
|
|
173
181
|
|
|
174
182
|
for (const specifier of specifiers) {
|
|
175
183
|
const target = path.resolve(generatedDir, specifier)
|
|
@@ -10,6 +10,7 @@ import { executeTool } from './tool-executor'
|
|
|
10
10
|
import { loadAllModuleTools, indexToolsForSearch } from './tool-loader'
|
|
11
11
|
import { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from './auth'
|
|
12
12
|
import { jsonSchemaToZod, toSafeZodSchema } from './schema-utils'
|
|
13
|
+
import { buildMcpToolAnnotations } from './mcp-tool-annotations'
|
|
13
14
|
import { redactSecretForLog, deriveApiKeySessionId } from './log-redaction'
|
|
14
15
|
import type { McpServerConfig, McpToolContext } from './types'
|
|
15
16
|
import type { SearchService } from '@open-mercato/search/service'
|
|
@@ -219,6 +220,7 @@ function createMcpServerForRequest(
|
|
|
219
220
|
{
|
|
220
221
|
description: tool.description,
|
|
221
222
|
inputSchema: safeSchema,
|
|
223
|
+
annotations: buildMcpToolAnnotations(tool),
|
|
222
224
|
},
|
|
223
225
|
async (args: unknown) => {
|
|
224
226
|
const toolArgs = (args ?? {}) as Record<string, unknown>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AwilixContainer } from 'awilix'
|
|
2
2
|
import type { z } from 'zod'
|
|
3
3
|
import { toolInputJsonSchema } from './tool-input-schema'
|
|
4
|
+
import { buildMcpToolAnnotations } from './mcp-tool-annotations'
|
|
4
5
|
import { getToolRegistry } from './tool-registry'
|
|
5
6
|
import { executeTool } from './tool-executor'
|
|
6
7
|
import { loadAllModuleTools } from './tool-loader'
|
|
@@ -138,6 +139,7 @@ export class InProcessMcpClient implements McpClientInterface {
|
|
|
138
139
|
name: tool.name,
|
|
139
140
|
description: tool.description,
|
|
140
141
|
inputSchema: toolInputJsonSchema(tool.inputSchema),
|
|
142
|
+
annotations: buildMcpToolAnnotations(tool),
|
|
141
143
|
}))
|
|
142
144
|
}
|
|
143
145
|
|
|
@@ -8,6 +8,7 @@ import { executeTool } from './tool-executor'
|
|
|
8
8
|
import { loadAllModuleTools, indexToolsForSearch } from './tool-loader'
|
|
9
9
|
import { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from './auth'
|
|
10
10
|
import { jsonSchemaToZod } from './schema-utils'
|
|
11
|
+
import { buildMcpToolAnnotations } from './mcp-tool-annotations'
|
|
11
12
|
import { getApiKeyFromMcpJson } from './mcp-dev-key-resolution'
|
|
12
13
|
import type { McpToolContext } from './types'
|
|
13
14
|
import type { SearchService } from '@open-mercato/search/service'
|
|
@@ -111,6 +112,7 @@ function createDevMcpServer(
|
|
|
111
112
|
{
|
|
112
113
|
description: tool.description,
|
|
113
114
|
inputSchema: safeSchema,
|
|
115
|
+
annotations: buildMcpToolAnnotations(tool),
|
|
114
116
|
},
|
|
115
117
|
async (args: unknown) => {
|
|
116
118
|
const toolArgs = (args ?? {}) as Record<string, unknown>
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
CallToolRequestSchema,
|
|
6
6
|
} from '@modelcontextprotocol/sdk/types.js'
|
|
7
7
|
import { toolInputJsonSchema } from './tool-input-schema'
|
|
8
|
+
import { buildMcpToolAnnotations } from './mcp-tool-annotations'
|
|
8
9
|
import { getToolRegistry } from './tool-registry'
|
|
9
10
|
import { executeTool } from './tool-executor'
|
|
10
11
|
import { loadAllModuleTools, indexToolsForSearch } from './tool-loader'
|
|
@@ -133,6 +134,7 @@ export async function createMcpServer(options: McpServerOptions): Promise<Server
|
|
|
133
134
|
name: tool.name,
|
|
134
135
|
description: tool.description,
|
|
135
136
|
inputSchema: toolInputJsonSchema(tool.inputSchema),
|
|
137
|
+
annotations: buildMcpToolAnnotations(tool),
|
|
136
138
|
})),
|
|
137
139
|
}
|
|
138
140
|
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AiToolDefinition, McpToolAnnotations, McpToolDefinition } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the MCP annotations advertised for a registered tool.
|
|
5
|
+
*
|
|
6
|
+
* `readOnlyHint` follows the registry's existing mutation contract: a tool is
|
|
7
|
+
* read-only unless it declares `isMutation: true`. Every write tool is required
|
|
8
|
+
* to set that flag (see the module AGENTS.md) and the agent mutation-policy gate
|
|
9
|
+
* already reads it the same way.
|
|
10
|
+
*
|
|
11
|
+
* `destructiveHint` only carries meaning for mutating tools. A predicate
|
|
12
|
+
* `isDestructive` cannot be evaluated without call input, so it resolves to
|
|
13
|
+
* `true` rather than under-warning the client. When the flag is absent the hint
|
|
14
|
+
* is omitted so clients fall back to the conservative MCP default (`true`).
|
|
15
|
+
*
|
|
16
|
+
* `idempotentHint` is deliberately not emitted: no tool-definition field
|
|
17
|
+
* carries that information, so clients keep the MCP default (`false`).
|
|
18
|
+
*/
|
|
19
|
+
export function buildMcpToolAnnotations(tool: McpToolDefinition): McpToolAnnotations {
|
|
20
|
+
const definition = tool as AiToolDefinition
|
|
21
|
+
|
|
22
|
+
if (definition.isMutation !== true) {
|
|
23
|
+
return { readOnlyHint: true }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { isDestructive } = definition
|
|
27
|
+
if (isDestructive === undefined) {
|
|
28
|
+
return { readOnlyHint: false }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
readOnlyHint: false,
|
|
33
|
+
destructiveHint: typeof isDestructive === 'function' ? true : isDestructive,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -249,6 +249,17 @@ export type ToolInfo = {
|
|
|
249
249
|
name: string
|
|
250
250
|
description: string
|
|
251
251
|
inputSchema: Record<string, unknown>
|
|
252
|
+
/** MCP capability hints advertised for the tool (see `buildMcpToolAnnotations`). */
|
|
253
|
+
annotations?: McpToolAnnotations
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** MCP `ToolAnnotations` values carried through remote and in-process clients. */
|
|
257
|
+
export type McpToolAnnotations = {
|
|
258
|
+
title?: string
|
|
259
|
+
readOnlyHint?: boolean
|
|
260
|
+
destructiveHint?: boolean
|
|
261
|
+
idempotentHint?: boolean
|
|
262
|
+
openWorldHint?: boolean
|
|
252
263
|
}
|
|
253
264
|
|
|
254
265
|
/**
|