@bevel-software/platform-core-backend 0.25.0 → 0.26.0
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/agent-guide/access-control.md +234 -0
- package/agent-guide/conventions.md +27 -0
- package/agent-guide/directory-structure.md +145 -0
- package/agent-guide/finding-things.md +7 -0
- package/agent-guide/introduction.md +27 -0
- package/agent-guide/skills.md +47 -0
- package/agent-guide/tool-manuals.md +217 -0
- package/agent-guide/where-a-new-file-goes.md +36 -0
- package/dist/assets.d.ts +7 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +9 -0
- package/dist/assets.js.map +1 -1
- package/dist/core/core-ports.d.ts +11 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +13 -2
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +9 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +14 -4
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +9 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -0
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +16 -0
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +139 -0
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.js +191 -0
- package/dist/modules/agent-guide/agent-guide.js.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts +24 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.js +100 -0
- package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -0
- package/dist/modules/agent-guide/index.d.ts +4 -0
- package/dist/modules/agent-guide/index.d.ts.map +1 -0
- package/dist/modules/agent-guide/index.js +4 -0
- package/dist/modules/agent-guide/index.js.map +1 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.js +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +9 -6
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +9 -6
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/index.d.ts +1 -1
- package/dist/modules/agent-instructions/index.d.ts.map +1 -1
- package/dist/modules/agent-instructions/index.js +1 -1
- package/dist/modules/agent-instructions/index.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +10 -50
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +32 -85
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +29 -2
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +113 -16
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/tool-schema-guard.d.ts +105 -0
- package/dist/modules/mcp/tool-schema-guard.d.ts.map +1 -0
- package/dist/modules/mcp/tool-schema-guard.js +171 -0
- package/dist/modules/mcp/tool-schema-guard.js.map +1 -0
- package/dist/modules/plugins/plugins.tools.d.ts +36 -2
- package/dist/modules/plugins/plugins.tools.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.tools.js +71 -14
- package/dist/modules/plugins/plugins.tools.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +0 -7
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +14 -53
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +3 -6
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +58 -16
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +23 -4
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -0
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +14 -0
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +7 -0
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +66 -36
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +14 -14
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
- package/dist/modules/tool-registry/description-length.js +24 -26
- package/dist/modules/tool-registry/description-length.js.map +1 -1
- package/dist/modules/tool-registry/guide-first.d.ts +23 -0
- package/dist/modules/tool-registry/guide-first.d.ts.map +1 -0
- package/dist/modules/tool-registry/guide-first.js +32 -0
- package/dist/modules/tool-registry/guide-first.js.map +1 -0
- package/dist/modules/tool-registry/tool-registry.d.ts +6 -0
- package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -1
- package/dist/modules/tool-registry/tool-registry.js +9 -2
- package/dist/modules/tool-registry/tool-registry.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts +449 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js +481 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +73 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +582 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +12 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js +5 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +9 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +210 -13
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +456 -91
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/merge-commit.d.ts +73 -0
- package/dist/modules/workflow/git/merge-commit.d.ts.map +1 -0
- package/dist/modules/workflow/git/merge-commit.js +89 -0
- package/dist/modules/workflow/git/merge-commit.js.map +1 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts +94 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +332 -37
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +35 -0
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +178 -12
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts +6 -2
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +7 -2
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +4 -0
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +3 -0
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +70 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +213 -20
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +22 -27
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.js +5 -3
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +10 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +211 -18
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +11 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +14 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/hidden-tools.d.ts +44 -0
- package/dist/shared/hidden-tools.d.ts.map +1 -0
- package/dist/shared/hidden-tools.js +13 -0
- package/dist/shared/hidden-tools.js.map +1 -0
- package/kb-template/.bevelignore +0 -5
- package/package.json +4 -3
- package/src/__tests__/kb-layout-config.test.ts +10 -100
- package/src/__tests__/packaged-assets-ship.test.ts +54 -0
- package/src/assets.ts +10 -0
- package/src/core/core-ports.ts +11 -0
- package/src/core/create-core-server.ts +13 -2
- package/src/core/create-core-services.ts +28 -4
- package/src/index.ts +2 -2
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +58 -0
- package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
- package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
- package/src/modules/access/access-control.interface.ts +15 -0
- package/src/modules/access/access-control.service.ts +21 -0
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +328 -0
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +189 -0
- package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
- package/src/modules/agent-guide/agent-guide.ts +291 -0
- package/src/modules/agent-guide/index.ts +21 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +28 -121
- package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
- package/src/modules/agent-instructions/compose.ts +9 -6
- package/src/modules/agent-instructions/index.ts +0 -3
- package/src/modules/agent-instructions/shared-file-rules.ts +31 -93
- package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +14 -3
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +250 -0
- package/src/modules/mcp/__tests__/mcp.service.test.ts +31 -23
- package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
- package/src/modules/mcp/mcp.service.ts +137 -19
- package/src/modules/mcp/tool-schema-guard.ts +196 -0
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +154 -4
- package/src/modules/plugins/plugins.tools.ts +75 -15
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +26 -55
- package/src/modules/settings/deployment-settings.service.ts +13 -54
- package/src/modules/settings/setup.routes.ts +3 -6
- package/src/modules/skills/__tests__/skills.tools.description.test.ts +91 -0
- package/src/modules/skills/skills.tools.ts +62 -16
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +57 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
- package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
- package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +74 -36
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +160 -0
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +61 -59
- package/src/modules/tool-registry/description-length.ts +24 -26
- package/src/modules/tool-registry/guide-first.ts +34 -0
- package/src/modules/tool-registry/tool-registry.ts +9 -2
- package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -1
- package/src/modules/workflow/agent-tools/__tests__/change-request-read-shape.test.ts +705 -0
- package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1518 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +23 -2
- package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +724 -0
- package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
- package/src/modules/workflow/agent-tools/workflow.tools.ts +8 -0
- package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
- package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +72 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
- package/src/modules/workflow/git/git.service.ts +537 -94
- package/src/modules/workflow/git/merge-commit.ts +88 -0
- package/src/modules/workflow/git/pull-request.service.ts +380 -54
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +7 -1
- package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +189 -11
- package/src/modules/workflow/workflow.routes.ts +7 -2
- package/src/modules/workflow/workflow.service.ts +7 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
- package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +21 -10
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +33 -55
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +255 -22
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +231 -1
- package/src/modules/workspace/startup/kb-startup-runner.ts +216 -19
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +191 -489
- package/src/modules/workspace/startup/steps/seed-tree.ts +21 -27
- package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
- package/src/modules/workspace/startup/steps/template-source.ts +5 -3
- package/src/modules/workspace/workspace.tools.ts +226 -16
- package/src/shared/domain-errors.ts +15 -0
- package/src/shared/hidden-tools.ts +45 -0
- package/kb-template/AGENTS.md +0 -730
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { inputSchemaDefect } from '@bevel-software/platform-mcp-core';
|
|
3
|
+
import { MAX_REMEMBERED_CALLERS, ToolSchemaGuard, type ScreenedTool } from '../tool-schema-guard.js';
|
|
4
|
+
|
|
5
|
+
const tool = (name: string, inputSchema: unknown): ScreenedTool => ({
|
|
6
|
+
utcpName: `notion.srv.${name}`,
|
|
7
|
+
mcpName: `notion_srv_${name}`,
|
|
8
|
+
inputSchema,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const VALID = { type: 'object', properties: { text: { type: 'string' } } };
|
|
12
|
+
/** `required` holding a number, in an `anyOf` branch — the Notion refusal. */
|
|
13
|
+
const INVALID = { type: 'object', properties: { value: { anyOf: [{ type: 'object', required: [7] }] } } };
|
|
14
|
+
|
|
15
|
+
/** One caller's load: the manuals on their surface, each with the tools it advertised. */
|
|
16
|
+
const load = (groups: Record<string, readonly ScreenedTool[]>) => new Map(Object.entries(groups));
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One load, with its own ticket. Every call takes a fresh one, so the order the
|
|
20
|
+
* tests screen in is the order the guard sees — which is what the two
|
|
21
|
+
* out-of-order tests below then break on purpose.
|
|
22
|
+
*/
|
|
23
|
+
const screen = (guard: ToolSchemaGuard, userId: string, groups: ReadonlyMap<string, readonly ScreenedTool[]>) =>
|
|
24
|
+
guard.screen(userId, guard.beginLoad(), groups);
|
|
25
|
+
|
|
26
|
+
describe('ToolSchemaGuard', () => {
|
|
27
|
+
it('names the tool, the place and the reason, and leaves its siblings alone', () => {
|
|
28
|
+
const guard = new ToolSchemaGuard();
|
|
29
|
+
const hidden = screen(guard, 'u1', load({ notion: [tool('a', VALID), tool('bad', INVALID), tool('b', VALID)] }));
|
|
30
|
+
|
|
31
|
+
expect([...hidden.keys()]).toEqual(['notion.srv.bad']);
|
|
32
|
+
// The UTCP name rides on what the LOAD gets back: the proxy takes the tool
|
|
33
|
+
// out of its repository by that name and records it in the audit trail by
|
|
34
|
+
// it, so a multi-segment `<manual>.<server>.<tool>` stays intact.
|
|
35
|
+
expect(hidden.get('notion.srv.bad')?.utcpName).toBe('notion.srv.bad');
|
|
36
|
+
// What the owner reads is about the tool by the name an agent calls it.
|
|
37
|
+
expect(guard.hiddenFor('notion')).toEqual([
|
|
38
|
+
{
|
|
39
|
+
manual: 'notion',
|
|
40
|
+
name: 'notion_srv_bad',
|
|
41
|
+
path: '/properties/value/anyOf/0/required/0',
|
|
42
|
+
reason: 'must be a string',
|
|
43
|
+
marker:
|
|
44
|
+
'Hidden from agents: its schema is invalid at /properties/value/anyOf/0/required/0 (must be a string).',
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('holds nothing for a server whose tools are all valid, and nothing for one never loaded', () => {
|
|
50
|
+
const guard = new ToolSchemaGuard();
|
|
51
|
+
screen(guard, 'u1', load({ notion: [tool('a', VALID)] }));
|
|
52
|
+
expect(guard.hiddenFor('notion')).toEqual([]);
|
|
53
|
+
expect(guard.hiddenFor('hubspot')).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('forgets the finding when the server sends a corrected schema', () => {
|
|
57
|
+
const guard = new ToolSchemaGuard();
|
|
58
|
+
screen(guard, 'u1', load({ notion: [tool('bad', INVALID)] }));
|
|
59
|
+
expect(guard.hiddenFor('notion')).toHaveLength(1);
|
|
60
|
+
|
|
61
|
+
screen(guard, 'u1', load({ notion: [tool('bad', VALID)] }));
|
|
62
|
+
expect(guard.hiddenFor('notion')).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A load with NO tools for a manual is the load that clears it, and there are
|
|
67
|
+
* two ways to get one: the server removed the offending tool, or the manual
|
|
68
|
+
* could not be attached at all and its tools were never loaded. Either way
|
|
69
|
+
* nothing should still be claiming a tool is hidden for a schema this process
|
|
70
|
+
* can no longer see.
|
|
71
|
+
*/
|
|
72
|
+
it('forgets the finding when the server advertises nothing at all', () => {
|
|
73
|
+
const guard = new ToolSchemaGuard();
|
|
74
|
+
screen(guard, 'u1', load({ notion: [tool('bad', INVALID)] }));
|
|
75
|
+
screen(guard, 'u1', load({ notion: [] }));
|
|
76
|
+
expect(guard.hiddenFor('notion')).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('forgets the finding when the manual leaves the surface entirely', () => {
|
|
80
|
+
const guard = new ToolSchemaGuard();
|
|
81
|
+
screen(guard, 'u1', load({ notion: [tool('bad', INVALID)], hubspot: [tool('fine', VALID)] }));
|
|
82
|
+
expect(guard.hiddenFor('notion')).toHaveLength(1);
|
|
83
|
+
|
|
84
|
+
screen(guard, 'u1', load({ hubspot: [tool('fine', VALID)] }));
|
|
85
|
+
expect(guard.hiddenFor('notion')).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('keeps each server to itself', () => {
|
|
89
|
+
const guard = new ToolSchemaGuard();
|
|
90
|
+
screen(guard, 'u1', load({ notion: [tool('bad', INVALID)], hubspot: [tool('fine', VALID)] }));
|
|
91
|
+
expect(guard.hiddenFor('notion')).toHaveLength(1);
|
|
92
|
+
expect(guard.hiddenFor('hubspot')).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Discovery runs on the REQUESTING user's own connection, so two callers can
|
|
97
|
+
* be shown different tools by one server. A finding is about the server, and
|
|
98
|
+
* the person who can fix it is not necessarily the person whose connection
|
|
99
|
+
* saw it — so one caller's load must never erase another's finding, and the
|
|
100
|
+
* owner-facing surfaces read the union.
|
|
101
|
+
*/
|
|
102
|
+
describe('with more than one caller', () => {
|
|
103
|
+
it('does not let one caller\'s load erase another\'s finding', () => {
|
|
104
|
+
const guard = new ToolSchemaGuard();
|
|
105
|
+
screen(guard, 'u1', load({ notion: [tool('bad', INVALID)] }));
|
|
106
|
+
// u2's connection is shown a different, healthy subset of the same server.
|
|
107
|
+
screen(guard, 'u2', load({ notion: [tool('a', VALID)] }));
|
|
108
|
+
expect(guard.hiddenFor('notion').map((t) => t.name)).toEqual(['notion_srv_bad']);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('reports a finding once, however many callers were shown it', () => {
|
|
112
|
+
const guard = new ToolSchemaGuard();
|
|
113
|
+
screen(guard, 'u1', load({ notion: [tool('bad', INVALID)] }));
|
|
114
|
+
screen(guard, 'u2', load({ notion: [tool('bad', INVALID)] }));
|
|
115
|
+
expect(guard.hiddenFor('notion')).toHaveLength(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('reports two different defects of one server together', () => {
|
|
119
|
+
const guard = new ToolSchemaGuard();
|
|
120
|
+
screen(guard, 'u1', load({ notion: [tool('bad', INVALID)] }));
|
|
121
|
+
screen(guard, 'u2', load({ notion: [tool('other', { type: 'object', properties: { x: { anyOf: {} } } })] }));
|
|
122
|
+
expect(guard.hiddenFor('notion').map((t) => t.name).sort()).toEqual([
|
|
123
|
+
'notion_srv_bad',
|
|
124
|
+
'notion_srv_other',
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Requests overlap — a caller's surface is rebuilt on every one of them, and
|
|
131
|
+
* two can be dialling the same server at once. So the order loads LAND in is
|
|
132
|
+
* not the order they READ in, and only the second one is evidence about the
|
|
133
|
+
* server's current schemas.
|
|
134
|
+
*/
|
|
135
|
+
describe('with loads that land out of order', () => {
|
|
136
|
+
it('does not let a load that read the server earlier overwrite a newer one', () => {
|
|
137
|
+
const guard = new ToolSchemaGuard();
|
|
138
|
+
const stale = guard.beginLoad(); // request A starts, the schema still broken
|
|
139
|
+
const fresh = guard.beginLoad(); // request B starts, after the vendor's fix
|
|
140
|
+
guard.screen('u1', fresh, load({ notion: [tool('bad', VALID)] }));
|
|
141
|
+
|
|
142
|
+
// A lands late, with what it saw. Its OWN request still keeps the tool off
|
|
143
|
+
// its surface — it has judged that schema invalid and must not offer it …
|
|
144
|
+
const found = guard.screen('u1', stale, load({ notion: [tool('bad', INVALID)] }));
|
|
145
|
+
expect([...found.keys()]).toEqual(['notion.srv.bad']);
|
|
146
|
+
// … but the marker everyone else reads is not put back on a tool that is
|
|
147
|
+
// now fine, to sit there until some later request happened to clear it.
|
|
148
|
+
expect(guard.hiddenFor('notion')).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('applies the newer load when the older one landed first', () => {
|
|
152
|
+
const guard = new ToolSchemaGuard();
|
|
153
|
+
const first = guard.beginLoad();
|
|
154
|
+
const second = guard.beginLoad();
|
|
155
|
+
guard.screen('u1', first, load({ notion: [tool('bad', INVALID)] }));
|
|
156
|
+
expect(guard.hiddenFor('notion')).toHaveLength(1);
|
|
157
|
+
|
|
158
|
+
guard.screen('u1', second, load({ notion: [tool('bad', VALID)] }));
|
|
159
|
+
expect(guard.hiddenFor('notion')).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('orders each caller against itself, not against the others', () => {
|
|
163
|
+
const guard = new ToolSchemaGuard();
|
|
164
|
+
const early = guard.beginLoad();
|
|
165
|
+
const late = guard.beginLoad();
|
|
166
|
+
// u2's NEWER load lands first, and u1's older one after it. Ordering kept
|
|
167
|
+
// globally rather than per caller would call u1's load stale on the
|
|
168
|
+
// strength of a ticket belonging to someone else, and drop a finding
|
|
169
|
+
// nothing else in this process has seen. The order matters: with u1's
|
|
170
|
+
// load first, both readings pass and the test proves nothing.
|
|
171
|
+
guard.screen('u2', late, load({ notion: [tool('a', VALID)] }));
|
|
172
|
+
guard.screen('u1', early, load({ notion: [tool('bad', INVALID)] }));
|
|
173
|
+
expect(guard.hiddenFor('notion').map((t) => t.name)).toEqual(['notion_srv_bad']);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The point of remembering. The MCP surface is rebuilt per request, so these
|
|
179
|
+
* same schemas come past on every `tools/list` AND every `tools/call`; the
|
|
180
|
+
* CHECK happens when a server's tools are loaded and when a refresh brings a
|
|
181
|
+
* schema this process has not seen, and never again.
|
|
182
|
+
*/
|
|
183
|
+
it('checks a schema once, however many times it is screened', () => {
|
|
184
|
+
const check = vi.fn(inputSchemaDefect);
|
|
185
|
+
const guard = new ToolSchemaGuard(check);
|
|
186
|
+
const tools = [tool('a', VALID), tool('bad', INVALID)];
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < 10; i += 1) screen(guard, 'u1', load({ notion: tools }));
|
|
189
|
+
expect(check).toHaveBeenCalledTimes(2);
|
|
190
|
+
|
|
191
|
+
// A CHANGED schema is a schema this process has not checked, so it is.
|
|
192
|
+
screen(guard, 'u1', load({ notion: [tool('a', { type: 'object', properties: { other: { type: 'number' } } })] }));
|
|
193
|
+
expect(check).toHaveBeenCalledTimes(3);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('checks two tools that share one schema once', () => {
|
|
197
|
+
const check = vi.fn(inputSchemaDefect);
|
|
198
|
+
const guard = new ToolSchemaGuard(check);
|
|
199
|
+
screen(guard, 'u1', load({ notion: [tool('a', VALID), tool('b', { ...VALID })] }));
|
|
200
|
+
expect(check).toHaveBeenCalledTimes(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('checks a schema once across callers, too — the verdict is the schema\'s', () => {
|
|
204
|
+
const check = vi.fn(inputSchemaDefect);
|
|
205
|
+
const guard = new ToolSchemaGuard(check);
|
|
206
|
+
screen(guard, 'u1', load({ notion: [tool('bad', INVALID)] }));
|
|
207
|
+
screen(guard, 'u2', load({ notion: [tool('bad', INVALID)] }));
|
|
208
|
+
expect(check).toHaveBeenCalledTimes(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('with more callers than it remembers', () => {
|
|
212
|
+
const MANY = MAX_REMEMBERED_CALLERS;
|
|
213
|
+
|
|
214
|
+
it('evicts the caller longest unseen, and keeps every other caller\'s finding on the owner\'s page', () => {
|
|
215
|
+
const guard = new ToolSchemaGuard();
|
|
216
|
+
// The first caller's finding, then a crowd of callers with nothing to
|
|
217
|
+
// report, then one more than fits.
|
|
218
|
+
screen(guard, 'first', load({ notion: [tool('bad', INVALID)] }));
|
|
219
|
+
for (let i = 1; i < MANY; i += 1) screen(guard, `u${i}`, load({ notion: [tool('a', VALID)] }));
|
|
220
|
+
expect(guard.hiddenFor('notion')).toHaveLength(1);
|
|
221
|
+
// `first` was seen again since — so it is not the longest unseen, and
|
|
222
|
+
// its finding stays when the crowd overflows.
|
|
223
|
+
screen(guard, 'first', load({ notion: [tool('bad', INVALID)] }));
|
|
224
|
+
screen(guard, 'overflow', load({ notion: [tool('a', VALID)] }));
|
|
225
|
+
expect(guard.hiddenFor('notion')).toHaveLength(1);
|
|
226
|
+
// Overflow once more: now `u1` goes, then `u2` — never the whole table.
|
|
227
|
+
screen(guard, 'overflow-2', load({ notion: [tool('a', VALID)] }));
|
|
228
|
+
expect(guard.hiddenFor('notion')).toHaveLength(1);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("keeps the order per caller: an evicted caller's next load is their newest, whichever began first", () => {
|
|
232
|
+
const guard = new ToolSchemaGuard();
|
|
233
|
+
for (let i = 0; i < MANY; i += 1) screen(guard, `u${i}`, load({ notion: [tool('a', VALID)] }));
|
|
234
|
+
// `u0` is the longest unseen. Two loads of theirs begin, old then new —
|
|
235
|
+
// and before either lands, the table overflows and `u0` is evicted,
|
|
236
|
+
// watermark and all.
|
|
237
|
+
const older = guard.beginLoad();
|
|
238
|
+
const newer = guard.beginLoad();
|
|
239
|
+
screen(guard, 'newcomer', load({ notion: [tool('a', VALID)] }));
|
|
240
|
+
// Nothing of `u0` is held, so whichever lands first is the newest
|
|
241
|
+
// picture this process has of them: the old load, with its finding.
|
|
242
|
+
guard.screen('u0', older, load({ notion: [tool('bad', INVALID)] }));
|
|
243
|
+
expect(guard.hiddenFor('notion')).toHaveLength(1);
|
|
244
|
+
// The newer load then lands with the corrected schema and wins —
|
|
245
|
+
// and the watermark it restores rejects anything older after it.
|
|
246
|
+
guard.screen('u0', newer, load({ notion: [tool('a', VALID)] }));
|
|
247
|
+
expect(guard.hiddenFor('notion')).toEqual([]);
|
|
248
|
+
const own = guard.screen('u0', older, load({ notion: [tool('bad', INVALID)] }));
|
|
249
|
+
expect([...own.keys()]).toEqual(['notion.srv.bad']);
|
|
250
|
+
expect(guard.hiddenFor('notion')).toEqual([]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("does not rank one caller's load against another caller's eviction", () => {
|
|
254
|
+
const guard = new ToolSchemaGuard();
|
|
255
|
+
// `x` begins a load; the table then fills and overflows, evicting
|
|
256
|
+
// callers whose tickets are newer than `x`'s.
|
|
257
|
+
const x = guard.beginLoad();
|
|
258
|
+
for (let i = 0; i < MANY; i += 1) screen(guard, `u${i}`, load({ notion: [tool('a', VALID)] }));
|
|
259
|
+
screen(guard, 'overflow', load({ notion: [tool('a', VALID)] }));
|
|
260
|
+
// `x`'s load is `x`'s newest, evictions elsewhere notwithstanding: its
|
|
261
|
+
// finding reaches the owner's page.
|
|
262
|
+
guard.screen('x', x, load({ notion: [tool('bad', INVALID)] }));
|
|
263
|
+
expect(guard.hiddenFor('notion')).toHaveLength(1);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
import { EXTERNAL_KB_MANUAL_NAME } from '../tool-manuals/tool-manuals.contract.js';
|
|
54
54
|
import type { IToolManualService } from '../tool-manuals/tool-manuals.contract.js';
|
|
55
55
|
import type { SpillStore } from '../workspace/spill-store.js';
|
|
56
|
+
import type { HiddenToolSource } from '../../shared/hidden-tools.js';
|
|
56
57
|
import { seedBevelHostedManualVars } from '../../shared/utcp-namespace.js';
|
|
57
58
|
import type { InternalTokenService } from '../tool-auth/internal-token.service.js';
|
|
58
59
|
import type { IAgentEventRecorder } from '../audit/audit.contract.js';
|
|
@@ -60,16 +61,17 @@ import { RequestAudit } from '../audit/request-audit.js';
|
|
|
60
61
|
import { ManualFailureMemo } from './manual-failure-memo.js';
|
|
61
62
|
import { DownstreamPool, POOL_KEY_SEPARATOR, type DownstreamPoolOptions, type Lease } from './downstream-pool.js';
|
|
62
63
|
import { SurfaceLogThrottle } from './surface-log-throttle.js';
|
|
64
|
+
import { ToolSchemaGuard, type ScreenedHiddenTool } from './tool-schema-guard.js';
|
|
63
65
|
import { DownstreamRefreshGuard, isDownstreamTokenRejection } from './downstream-token-refresh.js';
|
|
64
66
|
import { printable } from '../../shared/printable.js';
|
|
65
67
|
import {
|
|
66
68
|
composeAgentInstructions,
|
|
67
69
|
prefixToolDescription,
|
|
68
70
|
PREFIXED_TOOLS,
|
|
69
|
-
sharedRulesPointer,
|
|
70
71
|
type AgentPreambleReader,
|
|
71
72
|
type ComposedAgentInstructions,
|
|
72
73
|
} from '../agent-instructions/index.js';
|
|
74
|
+
import { guideFirstDescription } from '../tool-registry/guide-first.js';
|
|
73
75
|
|
|
74
76
|
/**
|
|
75
77
|
* Configuration for the loopback proxy. `loopbackBaseUrl` is the backend's own
|
|
@@ -95,8 +97,8 @@ export interface McpProxyOptions {
|
|
|
95
97
|
readAgentPreamble?: AgentPreambleReader;
|
|
96
98
|
/**
|
|
97
99
|
* The layout in effect, read per request: the shared file rules name the
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
+
* guide by the name a deployment saved for it, and the setup save applies
|
|
101
|
+
* a layout without a restart. A GETTER, so nothing snapshots the
|
|
100
102
|
* pre-setup default. Absent, the rules name `AGENTS.md`.
|
|
101
103
|
*/
|
|
102
104
|
kbLayout?: () => KbLayout;
|
|
@@ -166,6 +168,14 @@ interface RequestSurface {
|
|
|
166
168
|
* They differ only when the name holds a non-word character (`my-server`).
|
|
167
169
|
*/
|
|
168
170
|
catalogNames: ReadonlyMap<string, string>;
|
|
171
|
+
/**
|
|
172
|
+
* The tools this request's load took OFF the surface for an invalid schema,
|
|
173
|
+
* by the name an agent would have called them by. Carried on the surface
|
|
174
|
+
* rather than read back out of the guard: it is this caller's own load, so a
|
|
175
|
+
* call answered from it cannot answer about a schema some other caller's
|
|
176
|
+
* connection was shown.
|
|
177
|
+
*/
|
|
178
|
+
hidden: ReadonlyMap<string, ScreenedHiddenTool>;
|
|
169
179
|
}
|
|
170
180
|
|
|
171
181
|
/** A manual off the surface for want of a sign-in — see {@link RequestSurface.unavailable}. */
|
|
@@ -273,6 +283,15 @@ export class McpService {
|
|
|
273
283
|
*/
|
|
274
284
|
private readonly knownDownstreamTools = new Map<string, { definition: string; tools: UtcpTool[] }>();
|
|
275
285
|
|
|
286
|
+
/**
|
|
287
|
+
* The JSON Schema check on connected tools, and the memory of what it found.
|
|
288
|
+
* Process-wide rather than per request, because a schema's validity is a
|
|
289
|
+
* property of the server and not of the caller — and because the point of
|
|
290
|
+
* remembering is that the check runs when a server's tools are loaded, not
|
|
291
|
+
* once per request and never on a tool call. See {@link ToolSchemaGuard}.
|
|
292
|
+
*/
|
|
293
|
+
private readonly toolSchemas = new ToolSchemaGuard();
|
|
294
|
+
|
|
276
295
|
// The downstream connection pool for `mcp` manuals — see the class doc.
|
|
277
296
|
private readonly downstream: DownstreamPool<PooledDownstream>;
|
|
278
297
|
|
|
@@ -486,14 +505,17 @@ export class McpService {
|
|
|
486
505
|
// different name, and one fixed example is necessarily wrong on one of the
|
|
487
506
|
// two surfaces.
|
|
488
507
|
//
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
// `
|
|
493
|
-
//
|
|
508
|
+
// What a chain does with a failure, a large result or an image is
|
|
509
|
+
// stated once, in the shared rules — in the guide and in the handshake
|
|
510
|
+
// instructions — and not on the chain itself (an EMPTY pointer is how
|
|
511
|
+
// `mcp-core` is told the rules are served elsewhere). Like every tool of
|
|
512
|
+
// the platform's own, each meta-tool opens with the one sentence saying
|
|
513
|
+
// what to do before any of them, which is where those rules are. The
|
|
514
|
+
// registry puts the sentence on the tools it lists; the meta-tools are
|
|
515
|
+
// built here, so here it is.
|
|
494
516
|
const metaTools = codeModeMetaTools(EXTERNAL_KB_MANUAL_NAME, examplePool, {
|
|
495
|
-
sharedRulesPointer:
|
|
496
|
-
});
|
|
517
|
+
sharedRulesPointer: '',
|
|
518
|
+
}).map((tool) => ({ ...tool, description: guideFirstDescription(tool.description) }));
|
|
497
519
|
// Log only when a tool was dropped (name/schema/duplicate) — that's the
|
|
498
520
|
// anomaly worth surfacing, since a downstream client would otherwise hide
|
|
499
521
|
// it by rejecting the whole response.
|
|
@@ -510,7 +532,7 @@ export class McpService {
|
|
|
510
532
|
});
|
|
511
533
|
|
|
512
534
|
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
513
|
-
const { client, tools, unavailable, catalogNames } = await requestSurface();
|
|
535
|
+
const { client, tools, unavailable, catalogNames, hidden: hiddenTools } = await requestSurface();
|
|
514
536
|
const toolName = request.params.name;
|
|
515
537
|
const args = request.params.arguments ?? {};
|
|
516
538
|
if (META_TOOL_NAMES.has(toolName)) {
|
|
@@ -551,6 +573,24 @@ export class McpService {
|
|
|
551
573
|
};
|
|
552
574
|
const proxied = tools.find((t) => t.mcpName === toolName);
|
|
553
575
|
if (!proxied) {
|
|
576
|
+
// A tool this process hid for an invalid schema: say that, rather than
|
|
577
|
+
// "Unknown tool" about a tool the agent has every reason to think
|
|
578
|
+
// exists. The place and the reason are deliberately NOT here — they are
|
|
579
|
+
// for the people who manage the server, who are the only ones who can
|
|
580
|
+
// act on them.
|
|
581
|
+
const hidden = hiddenTools.get(toolName);
|
|
582
|
+
if (hidden) {
|
|
583
|
+
// The UTCP name, as every other audit path records a tool: the
|
|
584
|
+
// flattened agent name loses the server segment of a multi-segment
|
|
585
|
+
// `<manual>.<server>.<tool>`, and an audit trail that names a
|
|
586
|
+
// different tool than the rest of the trail is worse than none.
|
|
587
|
+
if (audit) await audit.denied(hidden.utcpName, args);
|
|
588
|
+
return toolError(
|
|
589
|
+
`The "${toolName}" tool is hidden from agents because its schema is invalid, so it cannot be ` +
|
|
590
|
+
`called. The people who manage its server can see the place in the schema and the reason, on ` +
|
|
591
|
+
`the tool's page in Hexis and through \`list_tool_setup\`.`,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
554
594
|
// Not on the surface — but if the name belongs to a manual that is off
|
|
555
595
|
// it only because this caller's sign-in is gone (and nothing in this
|
|
556
596
|
// process has seen its tools yet), the honest answer is the sign-in
|
|
@@ -626,7 +666,7 @@ export class McpService {
|
|
|
626
666
|
const manuals = await this.fetchManualTemplates(loopbackBearer);
|
|
627
667
|
const catalogMs = performance.now() - started;
|
|
628
668
|
const client = await this.buildClient(loopbackBearer, userId, manuals);
|
|
629
|
-
const { tools, unavailable, catalogNames } = await this.discoverTools(client, manuals, userId);
|
|
669
|
+
const { tools, unavailable, catalogNames, hidden } = await this.discoverTools(client, manuals, userId);
|
|
630
670
|
const totalMs = performance.now() - started;
|
|
631
671
|
// Per user: on a shape change or once per interval, never per request —
|
|
632
672
|
// see SurfaceLogThrottle for why both halves matter.
|
|
@@ -641,7 +681,7 @@ export class McpService {
|
|
|
641
681
|
(decision.suppressed > 0 ? ` [+${decision.suppressed} identical rebuild(s) since last line]` : ''),
|
|
642
682
|
);
|
|
643
683
|
}
|
|
644
|
-
return { client, tools, unavailable, catalogNames };
|
|
684
|
+
return { client, tools, unavailable, catalogNames, hidden };
|
|
645
685
|
}
|
|
646
686
|
|
|
647
687
|
/**
|
|
@@ -798,13 +838,37 @@ export class McpService {
|
|
|
798
838
|
client: CodeModeUtcpClient,
|
|
799
839
|
manuals: CallTemplate[],
|
|
800
840
|
userId: string,
|
|
801
|
-
): Promise<{
|
|
841
|
+
): Promise<{
|
|
842
|
+
tools: ProxiedTool[];
|
|
843
|
+
unavailable: UnavailableManual[];
|
|
844
|
+
catalogNames: Map<string, string>;
|
|
845
|
+
hidden: Map<string, ScreenedHiddenTool>;
|
|
846
|
+
}> {
|
|
802
847
|
const routes = new Map<string, DownstreamRoute>();
|
|
803
848
|
const unavailable: UnavailableManual[] = [];
|
|
849
|
+
// Taken BEFORE a single manual is registered, so it ranks this load by the
|
|
850
|
+
// freshness of what it is about to read — see ToolSchemaGuard.beginLoad.
|
|
851
|
+
const loadId = this.toolSchemas.beginLoad();
|
|
804
852
|
const catalogNames = new Map<string, string>();
|
|
805
853
|
for (const m of manuals) {
|
|
806
854
|
if (m.call_template_type === 'mcp') catalogNames.set(utcpManualName(m), String(m.name));
|
|
807
855
|
}
|
|
856
|
+
// Every manual's catalog name, which is how the owner-facing surfaces know
|
|
857
|
+
// it. `catalogNames` above is the credential catalog's map and covers `mcp`
|
|
858
|
+
// manuals only; the schema check is about the tools of every connected
|
|
859
|
+
// server, so it needs the whole list. Taken HERE, before registration:
|
|
860
|
+
// registering a manual renames the template in place, so `m.name` read
|
|
861
|
+
// afterwards is the rewritten identifier and `my-server` would be recorded
|
|
862
|
+
// as `my_server` — a name no tool page ever looks up.
|
|
863
|
+
// Keyed by the rewritten name, which two manuals can share (the collision
|
|
864
|
+
// handled below): the KB manual's entry is the one that stays, since it is
|
|
865
|
+
// the one that is registered when a `.tool` collides with it.
|
|
866
|
+
const manualCatalogNames = new Map<string, string>();
|
|
867
|
+
for (const m of manuals) {
|
|
868
|
+
const rewritten = utcpManualName(m);
|
|
869
|
+
if (manualCatalogNames.get(rewritten) === EXTERNAL_KB_MANUAL_NAME) continue;
|
|
870
|
+
manualCatalogNames.set(rewritten, String(m.name));
|
|
871
|
+
}
|
|
808
872
|
// The shared layer rewrites every manual name (`[^\w]` → `_`) and tools
|
|
809
873
|
// route by the rewritten prefix, so two manuals whose names rewrite to one
|
|
810
874
|
// identifier would silently share it. Sequential registration used to
|
|
@@ -884,11 +948,65 @@ export class McpService {
|
|
|
884
948
|
if (kbFailure && !kbFailure.ok) throw new Error(`Bevel tool discovery failed: ${kbFailure.error}`);
|
|
885
949
|
if (routes.size > 0) routeToDownstream(client, routes);
|
|
886
950
|
const utcpTools = await client.getTools();
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
951
|
+
const flattened = utcpTools.map((tool: UtcpTool) => flattenManualTool(tool, EXTERNAL_KB_MANUAL_NAME));
|
|
952
|
+
const { tools, hidden } = await this.hideInvalidSchemas(client, flattened, manualCatalogNames, userId, loadId);
|
|
953
|
+
return { tools, unavailable, catalogNames, hidden };
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Check the input schema of every tool the servers just advertised, and take
|
|
958
|
+
* the invalid ones OFF the client's tool repository.
|
|
959
|
+
*
|
|
960
|
+
* The repository is the single place `tools/list`, `list_tools`/`tools_info`
|
|
961
|
+
* and the TypeScript interfaces `call_tool_chain` generates are all built
|
|
962
|
+
* from, so removing a tool here removes it from all three — which is what
|
|
963
|
+
* "not offered to agents" has to mean. Its siblings are untouched: the check
|
|
964
|
+
* is per tool, so one bad schema costs that tool and nothing else of the
|
|
965
|
+
* server.
|
|
966
|
+
*
|
|
967
|
+
* A client would otherwise drop such a tool itself, silently, and the agent
|
|
968
|
+
* would be left without it and without a reason. Hidden here, the reason
|
|
969
|
+
* reaches the people who manage the server (see {@link ToolSchemaGuard}).
|
|
970
|
+
*/
|
|
971
|
+
private async hideInvalidSchemas(
|
|
972
|
+
client: CodeModeUtcpClient,
|
|
973
|
+
tools: ProxiedTool[],
|
|
974
|
+
catalogNames: ReadonlyMap<string, string>,
|
|
975
|
+
userId: string,
|
|
976
|
+
loadId: number,
|
|
977
|
+
): Promise<{ tools: ProxiedTool[]; hidden: Map<string, ScreenedHiddenTool> }> {
|
|
978
|
+
const byManual = new Map<string, ProxiedTool[]>();
|
|
979
|
+
// EVERY manual of this surface is screened, including the ones that
|
|
980
|
+
// advertised nothing. A server that removed its last invalid tool, and one
|
|
981
|
+
// whose tools never loaded at all (a sign-in that has gone), both come
|
|
982
|
+
// through here with an empty group — and an empty group is what clears the
|
|
983
|
+
// marker. Screening only the groups that have tools would leave the tool
|
|
984
|
+
// page, `list_tool_setup` and the hidden-tool answer on a call all
|
|
985
|
+
// reporting a defect this process can no longer see.
|
|
986
|
+
for (const manual of catalogNames.values()) byManual.set(manual, []);
|
|
987
|
+
for (const tool of tools) {
|
|
988
|
+
const manual = catalogNames.get(tool.manualName) ?? tool.manualName;
|
|
989
|
+
byManual.set(manual, [...(byManual.get(manual) ?? []), tool]);
|
|
990
|
+
}
|
|
991
|
+
const found = this.toolSchemas.screen(userId, loadId, byManual);
|
|
992
|
+
const hidden = new Map<string, ScreenedHiddenTool>();
|
|
993
|
+
for (const tool of found.values()) hidden.set(tool.name, tool);
|
|
994
|
+
if (found.size === 0) return { tools, hidden };
|
|
995
|
+
for (const utcpName of found.keys()) {
|
|
996
|
+
await client.config.tool_repository.removeTool(utcpName);
|
|
997
|
+
}
|
|
998
|
+
// Logged every time rather than throttled: a hidden tool is the one thing
|
|
999
|
+
// about this surface that a person has to act on, and it is rare.
|
|
1000
|
+
log.warn(
|
|
1001
|
+
`hiding ${found.size} connected tool(s) whose input schema is not valid JSON Schema: ` +
|
|
1002
|
+
`${[...found.values()].map((t) => `${t.name} (${t.path}: ${t.reason})`).join(', ')}`,
|
|
1003
|
+
);
|
|
1004
|
+
return { tools: tools.filter((tool) => !found.has(tool.utcpName)), hidden };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** The schema findings the owner-facing surfaces report — see {@link ToolSchemaGuard}. */
|
|
1008
|
+
get hiddenTools(): HiddenToolSource {
|
|
1009
|
+
return this.toolSchemas;
|
|
892
1010
|
}
|
|
893
1011
|
|
|
894
1012
|
/**
|