@bevel-software/platform-core-backend 0.7.5 → 0.8.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/LICENSE +202 -202
- package/THIRD-PARTY-NOTICES.md +428 -454
- package/dist/core/core-ports.d.ts +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +51 -7
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +5 -3
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +30 -18
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +8 -7
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +6 -6
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts +5 -5
- package/dist/modules/access/access-declarations.js +3 -3
- package/dist/modules/access/access-mutation.service.d.ts +3 -3
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +3 -3
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.js +4 -4
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.js +19 -19
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/creator-access.d.ts +2 -2
- package/dist/modules/access/creator-access.js +5 -5
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/access/roles-admin.service.d.ts +18 -4
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +14 -4
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
- package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode-names.js +5 -27
- package/dist/modules/code-mode/code-mode-names.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +29 -7
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +11 -52
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +33 -395
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/plugins/index.d.ts +7 -0
- package/dist/modules/plugins/index.d.ts.map +1 -0
- package/dist/modules/plugins/index.js +6 -0
- package/dist/modules/plugins/index.js.map +1 -0
- package/dist/modules/plugins/join-proposals.d.ts +53 -0
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
- package/dist/modules/plugins/join-proposals.js +67 -0
- package/dist/modules/plugins/join-proposals.js.map +1 -0
- package/dist/modules/plugins/join-requests.service.d.ts +81 -0
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
- package/dist/modules/plugins/join-requests.service.js +135 -0
- package/dist/modules/plugins/join-requests.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.js +344 -0
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +106 -0
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.contract.js +36 -0
- package/dist/modules/plugins/plugins.contract.js.map +1 -0
- package/dist/modules/plugins/plugins.routes.d.ts +42 -0
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.routes.js +379 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -0
- package/dist/modules/plugins/plugins.service.d.ts +60 -0
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.service.js +172 -0
- package/dist/modules/plugins/plugins.service.js.map +1 -0
- package/dist/modules/skills/pending-skills.service.d.ts +2 -2
- package/dist/modules/skills/pending-skills.service.js +7 -7
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +4 -4
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +1 -1
- package/dist/modules/skills/skills.service.js +5 -5
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.js +2 -2
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
- package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
- package/dist/modules/workspace/kb-seed.service.js +43 -10
- package/dist/modules/workspace/kb-seed.service.js.map +1 -1
- package/dist/modules/workspace/plugins-migration.d.ts +50 -0
- package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
- package/dist/modules/workspace/plugins-migration.js +379 -0
- package/dist/modules/workspace/plugins-migration.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.js +3 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +26 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +83 -12
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/kb-layout.test.js +3 -3
- package/dist/shared/kb-layout.test.js.map +1 -1
- package/dist/shared/utcp-namespace.d.ts +6 -27
- package/dist/shared/utcp-namespace.d.ts.map +1 -1
- package/dist/shared/utcp-namespace.js +6 -63
- package/dist/shared/utcp-namespace.js.map +1 -1
- package/dist/shared/variable-refs.d.ts +42 -0
- package/dist/shared/variable-refs.d.ts.map +1 -0
- package/dist/shared/variable-refs.js +60 -0
- package/dist/shared/variable-refs.js.map +1 -0
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +88 -35
- package/kb-template/KnowledgeBase/How to get started.md +10 -10
- package/kb-template/access.md +36 -36
- package/migrations/meta/0000_snapshot.json +1479 -1479
- package/package.json +5 -4
- package/src/assets.ts +25 -25
- package/src/core/core-ports.ts +106 -106
- package/src/core/create-core-server.ts +55 -9
- package/src/core/create-core-services.ts +40 -20
- package/src/index.ts +69 -69
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
- package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
- package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
- package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
- package/src/modules/access/__tests__/access-splice.test.ts +2 -2
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
- package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
- package/src/modules/access/__tests__/roles-admin.service.test.ts +13 -1
- package/src/modules/access/access-control.interface.ts +8 -7
- package/src/modules/access/access-control.service.ts +7 -7
- package/src/modules/access/access-declarations.ts +5 -5
- package/src/modules/access/access-mutation.service.ts +3 -3
- package/src/modules/access/access-splice.ts +4 -4
- package/src/modules/access/access.routes.ts +20 -20
- package/src/modules/access/creator-access.ts +5 -5
- package/src/modules/access/roles-admin.service.ts +13 -2
- package/src/modules/admin/admin-access.routes.ts +29 -29
- package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
- package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
- package/src/modules/auth/rate-limit.ts +45 -45
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
- package/src/modules/code-mode/code-mode-names.ts +10 -36
- package/src/modules/code-mode/code-mode.tool.ts +27 -7
- package/src/modules/database/connection.ts +15 -15
- package/src/modules/database/schema.ts +11 -11
- package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
- package/src/modules/mcp/mcp.service.ts +57 -435
- package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
- package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
- package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
- package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
- package/src/modules/plugins/index.ts +17 -0
- package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
- package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
- package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
- package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
- package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
- package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
- package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
- package/src/modules/skills/pending-skills.service.ts +7 -7
- package/src/modules/skills/skills.contract.ts +4 -4
- package/src/modules/skills/skills.service.ts +5 -5
- package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
- package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
- package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
- package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
- package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
- package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
- package/src/modules/workflow/git/git.service.ts +2 -2
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
- package/src/modules/workflow/workflow-hooks.ts +101 -101
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +81 -13
- package/src/modules/workspace/__tests__/plugins-migration.test.ts +427 -0
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
- package/src/modules/workspace/__tests__/workspace.service.test.ts +116 -0
- package/src/modules/workspace/bevel-ignore.ts +66 -66
- package/src/modules/workspace/kb-seed.service.ts +38 -9
- package/src/modules/workspace/plugins-migration.ts +479 -0
- package/src/modules/workspace/session-sink.ts +25 -25
- package/src/modules/workspace/workspace.routes.ts +3 -3
- package/src/modules/workspace/workspace.service.ts +85 -14
- package/src/modules/workspace/workspace.tools.ts +922 -922
- package/src/shared/__tests__/join-request.test.ts +13 -13
- package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
- package/src/shared/kb-layout.test.ts +3 -3
- package/src/shared/utcp-namespace.ts +10 -68
- package/src/shared/variable-refs.ts +64 -0
- package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
- package/src/modules/groups/index.ts +0 -17
- package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
- /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
|
@@ -19,11 +19,24 @@ import {
|
|
|
19
19
|
UtcpClientConfigSerializer,
|
|
20
20
|
CallTemplateSerializer,
|
|
21
21
|
type CallTemplate,
|
|
22
|
-
type JsonSchema,
|
|
23
22
|
type Tool as UtcpTool,
|
|
24
23
|
} from '@utcp/sdk';
|
|
25
24
|
import { CodeModeUtcpClient } from '@utcp/code-mode';
|
|
26
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
CODE_MODE_META_TOOLS,
|
|
27
|
+
META_TOOL_NAMES,
|
|
28
|
+
dispatchMetaTool,
|
|
29
|
+
dispatchToolCall,
|
|
30
|
+
registerManual,
|
|
31
|
+
flattenManualTool,
|
|
32
|
+
toListedTool,
|
|
33
|
+
toolError,
|
|
34
|
+
needsAuthorizationResult,
|
|
35
|
+
skillPromptText,
|
|
36
|
+
type ProxiedTool,
|
|
37
|
+
type SkillSummary,
|
|
38
|
+
type LoadedSkill,
|
|
39
|
+
} from '@bevel-software/platform-mcp-core';
|
|
27
40
|
import { bevelSecretsLoaderConfig } from '../secrets-vault/index.js';
|
|
28
41
|
import { scopesCovered, type ISecretsVaultService } from '../secrets-vault/secrets-vault.contract.js';
|
|
29
42
|
import { EXTERNAL_KB_MANUAL_NAME } from '../tool-manuals/tool-manuals.contract.js';
|
|
@@ -49,9 +62,6 @@ export interface McpProxyOptions {
|
|
|
49
62
|
publicFrontendUrl: string;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
|
-
/** Default cap on a `call_tool_chain` result's stringified size before it spills. */
|
|
53
|
-
const CALL_TOOL_CHAIN_MAX_OUTPUT = 200_000;
|
|
54
|
-
|
|
55
65
|
/**
|
|
56
66
|
* Upper bound on one `loopbackJson` round-trip (manual list, skill fetch) so a
|
|
57
67
|
* hung loopback can't stall `createSession`. Generous: these endpoints answer
|
|
@@ -71,85 +81,6 @@ const MCP_LOOPBACK_TOKEN_TTL_MS = 5 * 60 * 60 * 1000;
|
|
|
71
81
|
|
|
72
82
|
const callTemplateSerializer = new CallTemplateSerializer();
|
|
73
83
|
|
|
74
|
-
/**
|
|
75
|
-
* Code-mode meta-tools exposed ALONGSIDE the direct tools. They let an external
|
|
76
|
-
* agent batch many Bevel calls into one isolated-vm run (`call_tool_chain`)
|
|
77
|
-
* instead of one MCP round-trip per call — the same efficiency our own agent
|
|
78
|
-
* gets. `call_tool_chain`'s description carries the code-mode protocol (there is
|
|
79
|
-
* no system prompt over MCP), so the client learns the convention from the tool
|
|
80
|
-
* itself; `list_tools`/`tools_info` are how it discovers what to call.
|
|
81
|
-
*
|
|
82
|
-
* Security is identical to the direct surface: the chain runs in our isolated-vm
|
|
83
|
-
* but calls tools over loopback with the CALLER's key against the external
|
|
84
|
-
* catalog — internal-only tools aren't in that catalog, so a chain can't reach
|
|
85
|
-
* them either.
|
|
86
|
-
*/
|
|
87
|
-
const CALL_TOOL_CHAIN_DESCRIPTION = [
|
|
88
|
-
'Execute a short JavaScript program with direct access to every registered UTCP tool as a synchronous function. Call tools as `KNOWLEDGE_BASE.<tool>({ body: { ...args } })` with NO `await` (results are already resolved), and `return` the final value. The runtime is plain JavaScript (no type annotations / no TypeScript-only syntax).',
|
|
89
|
-
'Discover first: `list_tools` lists every tool in callable form (e.g. `KNOWLEDGE_BASE.read_file`); `tools_info` returns their exact argument + return shapes — do not guess. Batch multiple tool calls into one chain to avoid a round-trip per call. The chain runs with your own connection key, so it can only reach the tools you can already call directly.',
|
|
90
|
-
'Large results: if the combined result+logs exceed `max_output_size` (default 200000 chars) the full JSON is spilled to a shared store and you get back a `__tool_chain_spill__/…` ref instead. Read it with `read_file` (pass that ref as `path` — `branch` is ignored — plus `offset`/`limit` to slice it), or better, re-run a narrower chain that returns only what you need.',
|
|
91
|
-
].join('\n\n');
|
|
92
|
-
|
|
93
|
-
const CODE_MODE_META_TOOLS: McpTool[] = [
|
|
94
|
-
{
|
|
95
|
-
name: 'list_tools',
|
|
96
|
-
description:
|
|
97
|
-
'List every UTCP tool currently registered, in TypeScript-accessible form (e.g. `KNOWLEDGE_BASE.read_file`) for use inside `call_tool_chain`.',
|
|
98
|
-
inputSchema: { type: 'object', properties: {}, additionalProperties: false } as McpTool['inputSchema'],
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
name: 'tools_info',
|
|
102
|
-
description:
|
|
103
|
-
'Get full TypeScript interface definitions for named tools (names from `list_tools`). The schemas are the source of truth — do not guess shapes.',
|
|
104
|
-
inputSchema: {
|
|
105
|
-
type: 'object',
|
|
106
|
-
properties: {
|
|
107
|
-
tool_names: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Tool names to describe.' },
|
|
108
|
-
},
|
|
109
|
-
required: ['tool_names'],
|
|
110
|
-
additionalProperties: false,
|
|
111
|
-
} as McpTool['inputSchema'],
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
name: 'call_tool_chain',
|
|
115
|
-
description: CALL_TOOL_CHAIN_DESCRIPTION,
|
|
116
|
-
inputSchema: {
|
|
117
|
-
type: 'object',
|
|
118
|
-
properties: {
|
|
119
|
-
code: { type: 'string', minLength: 1, description: 'JavaScript to execute against the registered tools.' },
|
|
120
|
-
timeout: { type: 'integer', minimum: 1000, maximum: 120000, description: 'Timeout in ms (default 30000).' },
|
|
121
|
-
max_output_size: { type: 'integer', minimum: 1000, maximum: 1000000, description: 'Max result+logs size in chars before spilling (default 200000, max 1000000).' },
|
|
122
|
-
},
|
|
123
|
-
required: ['code'],
|
|
124
|
-
additionalProperties: false,
|
|
125
|
-
} as McpTool['inputSchema'],
|
|
126
|
-
},
|
|
127
|
-
];
|
|
128
|
-
|
|
129
|
-
const META_TOOL_NAMES = new Set(CODE_MODE_META_TOOLS.map((t) => t.name));
|
|
130
|
-
|
|
131
|
-
/** Minimal skill shapes the prompt bridge needs (kept local so the proxy stays decoupled from `modules/skills`). */
|
|
132
|
-
interface SkillSummary {
|
|
133
|
-
name: string;
|
|
134
|
-
description: string;
|
|
135
|
-
}
|
|
136
|
-
interface LoadedSkill extends SkillSummary {
|
|
137
|
-
body: string;
|
|
138
|
-
path: string;
|
|
139
|
-
files: string[];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** The prompt message text for a loaded skill: its instructions body + a pointer to bundled files. */
|
|
143
|
-
function skillPromptText(skill: LoadedSkill): string {
|
|
144
|
-
const rel = skill.files.map((f) =>
|
|
145
|
-
f.startsWith(`${skill.path}/`) ? f.slice(skill.path.length + 1) : f,
|
|
146
|
-
);
|
|
147
|
-
const footer = rel.length
|
|
148
|
-
? `\n\n---\nSkill folder: ${skill.path}\nBundled files (fetch each with the get_skill tool: { name: "${skill.name}", file }): ${rel.join(', ')}`
|
|
149
|
-
: '';
|
|
150
|
-
return `${skill.body}${footer}`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
84
|
/**
|
|
154
85
|
* The MCP server is a GENERIC proxy over the UTCP tool surface. It owns no tool
|
|
155
86
|
* logic: per MCP session it stands up a `UtcpClient` pointed at the backend's
|
|
@@ -544,27 +475,20 @@ export class McpService {
|
|
|
544
475
|
// memo while registration is in flight, the stale failure from the OLD
|
|
545
476
|
// credential must not resurrect an entry the clear removed.
|
|
546
477
|
const generation = this.manualFailures.currentGeneration;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
} catch (err) {
|
|
558
|
-
// Registration (network/discovery) failure — the templates are already
|
|
559
|
-
// validated, so this is a runtime, not a schema, problem.
|
|
560
|
-
if (isKb) throw err;
|
|
561
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
562
|
-
this.manualFailures.recordFailure(userId, name, message, generation);
|
|
563
|
-
console.warn(`[mcp] skipping manual "${name}": ${message}`);
|
|
478
|
+
// `registerManual` never throws: a discovery/network failure and a
|
|
479
|
+
// validation failure both come back as `{ ok: false }`, because the retry
|
|
480
|
+
// policy — this memo — is ours, not the shared layer's.
|
|
481
|
+
const result = await registerManual(client, m);
|
|
482
|
+
if (!result.ok) {
|
|
483
|
+
if (isKb) throw new Error(`Bevel tool discovery failed: ${result.error}`);
|
|
484
|
+
this.manualFailures.recordFailure(userId, name, result.error, generation);
|
|
485
|
+
console.warn(`[mcp] skipping manual "${name}": ${result.error}`);
|
|
486
|
+
} else if (!isKb) {
|
|
487
|
+
this.manualFailures.clear(userId, name);
|
|
564
488
|
}
|
|
565
489
|
}
|
|
566
490
|
const utcpTools = await client.getTools();
|
|
567
|
-
return utcpTools.map((tool: UtcpTool) => flattenManualTool(tool));
|
|
491
|
+
return utcpTools.map((tool: UtcpTool) => flattenManualTool(tool, EXTERNAL_KB_MANUAL_NAME));
|
|
568
492
|
}
|
|
569
493
|
|
|
570
494
|
/**
|
|
@@ -661,351 +585,49 @@ export class McpService {
|
|
|
661
585
|
// handler used; the strict ServerNotification type requires the token.
|
|
662
586
|
extra: { sessionId?: string; sendNotification: (n: any) => Promise<void> },
|
|
663
587
|
): Promise<CallToolResult> {
|
|
664
|
-
const args = request.params.arguments ?? {};
|
|
665
|
-
|
|
666
588
|
const progressToken = request.params._meta?.progressToken;
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
// would be wrong for at least one protocol.
|
|
678
|
-
for await (const chunk of client.callToolStreaming(tool.utcpName, args)) {
|
|
679
|
-
if (hasPrev) {
|
|
680
|
-
progress += 1;
|
|
681
|
-
await extra
|
|
682
|
-
.sendNotification({
|
|
683
|
-
method: 'notifications/progress',
|
|
684
|
-
params: {
|
|
685
|
-
...(progressToken !== undefined ? { progressToken } : {}),
|
|
686
|
-
progress,
|
|
687
|
-
message: renderProgress(prev),
|
|
688
|
-
},
|
|
689
|
-
})
|
|
690
|
-
.catch((err) => console.warn('[mcp] progress notification failed:', err));
|
|
691
|
-
}
|
|
692
|
-
prev = chunk;
|
|
693
|
-
hasPrev = true;
|
|
694
|
-
}
|
|
695
|
-
} catch (err) {
|
|
696
|
-
return toolError(`The "${tool.mcpName}" tool failed: ${describeToolFailure(err)}`);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
return toCallToolResult(prev);
|
|
589
|
+
return dispatchToolCall(client, tool, request.params.arguments ?? {}, (progress, message) =>
|
|
590
|
+
extra.sendNotification({
|
|
591
|
+
method: 'notifications/progress',
|
|
592
|
+
params: {
|
|
593
|
+
...(progressToken !== undefined ? { progressToken } : {}),
|
|
594
|
+
progress,
|
|
595
|
+
message,
|
|
596
|
+
},
|
|
597
|
+
}),
|
|
598
|
+
);
|
|
700
599
|
}
|
|
701
600
|
|
|
702
601
|
/**
|
|
703
|
-
* Handle a code-mode meta-tool.
|
|
704
|
-
* session
|
|
705
|
-
*
|
|
706
|
-
*
|
|
602
|
+
* Handle a code-mode meta-tool. The shared implementation reflects on this
|
|
603
|
+
* session's client, so `list_tools`/`tools_info` describe exactly the catalog
|
|
604
|
+
* this session discovered and `call_tool_chain` runs in that client's
|
|
605
|
+
* isolated-vm — resolving over loopback with the caller's key. The workspace
|
|
606
|
+
* spill store is passed in so an oversized chain result comes back as a
|
|
607
|
+
* `read_file`-able ref rather than a wall of JSON.
|
|
707
608
|
*/
|
|
708
609
|
private async dispatchMetaTool(
|
|
709
610
|
client: CodeModeUtcpClient,
|
|
710
611
|
name: string,
|
|
711
612
|
args: Record<string, unknown>,
|
|
712
613
|
): Promise<CallToolResult> {
|
|
713
|
-
|
|
714
|
-
if (name === 'list_tools') {
|
|
715
|
-
const tools = await client.config.tool_repository.getTools();
|
|
716
|
-
return toCallToolResult({ tools: tools.map((t) => utcpNameToTsInterfaceName(t.name)) });
|
|
717
|
-
}
|
|
718
|
-
if (name === 'tools_info') {
|
|
719
|
-
const names = Array.isArray(args.tool_names) ? (args.tool_names as string[]) : [];
|
|
720
|
-
const interfaces: string[] = [];
|
|
721
|
-
const notFound: string[] = [];
|
|
722
|
-
for (const n of names) {
|
|
723
|
-
const found = await findToolByName(client, n);
|
|
724
|
-
if (found) interfaces.push(client.toolToTypeScriptInterface(found.tool));
|
|
725
|
-
else notFound.push(n);
|
|
726
|
-
}
|
|
727
|
-
return toCallToolResult({ interfaces: interfaces.join('\n\n'), not_found: notFound });
|
|
728
|
-
}
|
|
729
|
-
// call_tool_chain
|
|
730
|
-
const code = typeof args.code === 'string' ? args.code : '';
|
|
731
|
-
const timeout = typeof args.timeout === 'number' ? args.timeout : 30_000;
|
|
732
|
-
// Clamp to [1000, 1_000_000] so a caller can't force oversized inline
|
|
733
|
-
// output past the spill (schema bounds are advisory over a raw JSON-RPC call).
|
|
734
|
-
const maxOutputSize =
|
|
735
|
-
typeof args.max_output_size === 'number'
|
|
736
|
-
? Math.min(1_000_000, Math.max(1_000, Math.trunc(args.max_output_size)))
|
|
737
|
-
: CALL_TOOL_CHAIN_MAX_OUTPUT;
|
|
738
|
-
const { result, logs } = await client.callToolChain(code, timeout);
|
|
739
|
-
// Bound the payload: an external session has no ambient workspace, so an
|
|
740
|
-
// oversized result spills to the shared store and we return only a ref —
|
|
741
|
-
// parity with the in-process agent's `call_tool_chain`.
|
|
742
|
-
if (JSON.stringify({ success: true, result, logs }).length <= maxOutputSize) {
|
|
743
|
-
return toCallToolResult({ success: true, result, logs });
|
|
744
|
-
}
|
|
745
|
-
const fullJson = JSON.stringify({ result, logs }, null, 2);
|
|
746
|
-
const { ref, bytes } = await this.opts.spillStore.write(fullJson);
|
|
747
|
-
return toCallToolResult({
|
|
748
|
-
success: true,
|
|
749
|
-
truncated: true,
|
|
750
|
-
result_ref: ref,
|
|
751
|
-
result_bytes: bytes,
|
|
752
|
-
message: `Result+logs payload was ${fullJson.length} characters (exceeded max_output_size of ${maxOutputSize}). Full JSON saved to the shared spill store as \`${ref}\`. Read it back with \`read_file\` (pass that ref as \`path\`, \`branch\` ignored, plus \`offset\`/\`limit\` to slice), or re-run a narrower chain that returns only what you need.`,
|
|
753
|
-
});
|
|
754
|
-
} catch (err) {
|
|
755
|
-
return toolError(`The "${name}" tool failed: ${describeToolFailure(err)}`);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/** A tool discovered from the manual, flattened into what the proxy advertises. */
|
|
761
|
-
export interface ProxiedTool {
|
|
762
|
-
utcpName: string;
|
|
763
|
-
mcpName: string;
|
|
764
|
-
description: string;
|
|
765
|
-
inputSchema: JsonSchema;
|
|
766
|
-
/** The UTCP manual this tool came from (the `<manual>` in `<manual>.<tool>`),
|
|
767
|
-
* used to look up the manual's declared per-user credentials before dispatch. */
|
|
768
|
-
manualName: string;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* Flatten a discovered UTCP tool (`<manual>.<tool>`) into the proxy's advertised
|
|
773
|
-
* shape when MULTIPLE manuals are registered. KB tools keep their bare name (so
|
|
774
|
-
* existing external agents still call `read_file`); a user `.tool`'s tool is
|
|
775
|
-
* namespaced as `<manual>_<tool>` to guarantee a unique, dot-free MCP name.
|
|
776
|
-
*/
|
|
777
|
-
/**
|
|
778
|
-
* MCP tool-name grammar (also the Anthropic API's), and a length bound. A
|
|
779
|
-
* remote MCP server can expose a tool whose flattened name breaks this — too
|
|
780
|
-
* long, or an illegal char the `<manual>_<name>` flattening didn't remove — and
|
|
781
|
-
* an MCP client (or the model API behind it) rejects the ENTIRE `tools/list`
|
|
782
|
-
* response when a single entry is non-conforming. That makes EVERY tool vanish
|
|
783
|
-
* the moment one bad tool from a newly-added server enters the catalog, with no
|
|
784
|
-
* server-side error (the rejection is the client's). `toListedTool` isolates it
|
|
785
|
-
* per tool: drop the offender (logged), normalize an odd schema, keep the rest.
|
|
786
|
-
*/
|
|
787
|
-
const MCP_TOOL_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
788
|
-
// The Anthropic API caps a tool name at 128 chars — but the MCP CLIENT (Claude
|
|
789
|
-
// Code, claude.ai) prepends `mcp__<server>__` (≈20+ chars) before sending it,
|
|
790
|
-
// and that FULL name is what the 128 applies to. So budget for the prefix here,
|
|
791
|
-
// or a long `googlecalendar_…` name we pass gets the whole request 400'd. This
|
|
792
|
-
// is deliberately conservative; a dropped tool is logged so it's diagnosable.
|
|
793
|
-
const MCP_TOOL_NAME_MAX = 100;
|
|
794
|
-
|
|
795
|
-
/** A discovered tool as an MCP listing entry, or null if its name can't be listed. */
|
|
796
|
-
export function toListedTool(tool: ProxiedTool): McpTool | null {
|
|
797
|
-
if (!MCP_TOOL_NAME_RE.test(tool.mcpName) || tool.mcpName.length > MCP_TOOL_NAME_MAX) {
|
|
798
|
-
console.warn(
|
|
799
|
-
`[mcp] dropping tool "${tool.mcpName}" from the listing — not a valid MCP tool name ` +
|
|
800
|
-
`(must match ${MCP_TOOL_NAME_RE} and be ≤${MCP_TOOL_NAME_MAX} chars). ` +
|
|
801
|
-
'One non-conforming tool would otherwise make the whole toolset disappear on the client.',
|
|
802
|
-
);
|
|
803
|
-
return null;
|
|
614
|
+
return dispatchMetaTool(client, name, args, this.opts.spillStore);
|
|
804
615
|
}
|
|
805
|
-
// MCP requires an object inputSchema. A remote server's schema that isn't a
|
|
806
|
-
// plain object (or omits `type: 'object'`) can invalidate the whole response,
|
|
807
|
-
// so normalize it — keeping any declared properties — rather than pass it
|
|
808
|
-
// through verbatim.
|
|
809
|
-
const raw = tool.inputSchema;
|
|
810
|
-
let inputSchema: Record<string, unknown> =
|
|
811
|
-
raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
812
|
-
? { type: 'object', ...(raw as Record<string, unknown>) }
|
|
813
|
-
: { type: 'object', properties: {} };
|
|
814
|
-
// Sanitize the schema into what the Anthropic tool validator accepts. A
|
|
815
|
-
// remote server that emits a construct the validator rejects — Google's
|
|
816
|
-
// gmail/calendar use `$ref`/`$defs` AND OpenAPI `format` values like
|
|
817
|
-
// `int32`/`byte` — makes the CLIENT reject the ENTIRE tools/list response,
|
|
818
|
-
// so all tools vanish and nothing registers. Sanitizing per-tool means one
|
|
819
|
-
// odd server can't blank the whole toolset.
|
|
820
|
-
inputSchema = sanitizeInputSchema(inputSchema) as Record<string, unknown>;
|
|
821
|
-
// The MCP/Anthropic validator requires the top-level `type` to be exactly
|
|
822
|
-
// "object" and (for the Anthropic API) `properties` to be present. Force both
|
|
823
|
-
// so a remote schema that declared something else — or a union like
|
|
824
|
-
// `["object","null"]` — can't reject the whole tools/list.
|
|
825
|
-
inputSchema.type = 'object';
|
|
826
|
-
if (typeof inputSchema.properties !== 'object' || inputSchema.properties === null) {
|
|
827
|
-
inputSchema.properties = {};
|
|
828
|
-
}
|
|
829
|
-
return {
|
|
830
|
-
name: tool.mcpName,
|
|
831
|
-
description: tool.description,
|
|
832
|
-
inputSchema: inputSchema as McpTool['inputSchema'],
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
/** JSON-Schema string `format` values the Anthropic tool validator accepts. */
|
|
837
|
-
const SUPPORTED_SCHEMA_FORMATS = new Set([
|
|
838
|
-
'date-time',
|
|
839
|
-
'time',
|
|
840
|
-
'date',
|
|
841
|
-
'duration',
|
|
842
|
-
'email',
|
|
843
|
-
'hostname',
|
|
844
|
-
'uri',
|
|
845
|
-
'ipv4',
|
|
846
|
-
'ipv6',
|
|
847
|
-
'uuid',
|
|
848
|
-
]);
|
|
849
|
-
|
|
850
|
-
/**
|
|
851
|
-
* Make a remote server's JSON Schema safe for the Anthropic tool validator:
|
|
852
|
-
* - inline local `$ref` pointers (`#/$defs/...`, `#/definitions/...`) and drop
|
|
853
|
-
* the now-unreferenced `$defs`/`definitions` blocks (the API restricts `$ref`
|
|
854
|
-
* and MCP clients converting our schemas reject it outright);
|
|
855
|
-
* - drop non-standard `format` values (OpenAPI's `int32`/`byte`/… — only the
|
|
856
|
-
* JSON-Schema-standard formats above are accepted; `format` is advisory, so
|
|
857
|
-
* dropping it doesn't change tool behavior).
|
|
858
|
-
* Depth-bounded so a recursive schema degrades to a permissive `{}` node instead
|
|
859
|
-
* of hanging or emitting the unsupported recursion; non-local/external refs
|
|
860
|
-
* degrade the same way. Exported for direct testing.
|
|
861
|
-
*/
|
|
862
|
-
export function sanitizeInputSchema(schema: unknown): unknown {
|
|
863
|
-
const root = schema;
|
|
864
|
-
const resolvePointer = (pointer: string): unknown => {
|
|
865
|
-
if (!pointer.startsWith('#/')) return undefined;
|
|
866
|
-
let node: unknown = root;
|
|
867
|
-
for (const partRaw of pointer.slice(2).split('/')) {
|
|
868
|
-
const part = partRaw.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
869
|
-
if (!node || typeof node !== 'object') return undefined;
|
|
870
|
-
node = (node as Record<string, unknown>)[part];
|
|
871
|
-
}
|
|
872
|
-
return node;
|
|
873
|
-
};
|
|
874
|
-
const walk = (node: unknown, depth: number): unknown => {
|
|
875
|
-
if (depth > 20) return {}; // recursion/cycle guard — permissive fallback
|
|
876
|
-
if (Array.isArray(node)) return node.map((item) => walk(item, depth + 1));
|
|
877
|
-
if (!node || typeof node !== 'object') return node;
|
|
878
|
-
const obj = node as Record<string, unknown>;
|
|
879
|
-
if (typeof obj.$ref === 'string') {
|
|
880
|
-
const target = resolvePointer(obj.$ref);
|
|
881
|
-
// JSON Schema allows siblings next to $ref; keep them, target wins ties.
|
|
882
|
-
const { $ref: _ref, ...siblings } = obj;
|
|
883
|
-
const resolved = walk(target ?? {}, depth + 1);
|
|
884
|
-
return resolved && typeof resolved === 'object' && !Array.isArray(resolved)
|
|
885
|
-
? { ...siblings, ...(resolved as Record<string, unknown>) }
|
|
886
|
-
: Object.keys(siblings).length
|
|
887
|
-
? siblings
|
|
888
|
-
: resolved ?? {};
|
|
889
|
-
}
|
|
890
|
-
const out: Record<string, unknown> = {};
|
|
891
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
892
|
-
if (key === '$defs' || key === 'definitions') continue; // inlined above
|
|
893
|
-
// Drop a non-standard `format` (OpenAPI `int32`/`byte`/…) — the validator
|
|
894
|
-
// only allows the JSON-Schema-standard set; the annotation is non-load-bearing.
|
|
895
|
-
if (key === 'format' && (typeof value !== 'string' || !SUPPORTED_SCHEMA_FORMATS.has(value))) {
|
|
896
|
-
continue;
|
|
897
|
-
}
|
|
898
|
-
out[key] = walk(value, depth + 1);
|
|
899
|
-
}
|
|
900
|
-
return out;
|
|
901
|
-
};
|
|
902
|
-
return walk(schema, 0);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
export function flattenManualTool(tool: UtcpTool): ProxiedTool {
|
|
906
|
-
const dot = tool.name.indexOf('.');
|
|
907
|
-
const manual = dot >= 0 ? tool.name.slice(0, dot) : '';
|
|
908
|
-
const bare = dot >= 0 ? tool.name.slice(dot + 1) : tool.name;
|
|
909
|
-
const mcpName = manual === EXTERNAL_KB_MANUAL_NAME ? bare : tool.name.replace(/\./g, '_');
|
|
910
|
-
return {
|
|
911
|
-
utcpName: tool.name,
|
|
912
|
-
mcpName,
|
|
913
|
-
description: tool.description,
|
|
914
|
-
inputSchema: tool.inputs,
|
|
915
|
-
manualName: manual,
|
|
916
|
-
};
|
|
917
616
|
}
|
|
918
617
|
|
|
919
618
|
/**
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
619
|
+
* The pieces of the proxy that are shared with the local MCP server now live in
|
|
620
|
+
* `@bevel-software/platform-mcp-core`. Re-exported here because this module is
|
|
621
|
+
* where they have always been imported from — inside this package and by its
|
|
622
|
+
* tests — and moving a file is not a reason to churn every call site.
|
|
924
623
|
*/
|
|
925
|
-
export
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
/**
|
|
937
|
-
* Extract a human-meaningful failure message from a tool-call error. UTCP's
|
|
938
|
-
* HTTP protocol surfaces a non-2xx as an axios-style error whose `.response.data`
|
|
939
|
-
* is the REST endpoint's JSON body (`{ error: "..." }`). Pull that out so the
|
|
940
|
-
* MCP caller sees the tool's real message instead of a bare "status code 500".
|
|
941
|
-
*/
|
|
942
|
-
export function describeToolFailure(err: unknown): string {
|
|
943
|
-
const data = (err as { response?: { data?: unknown } })?.response?.data;
|
|
944
|
-
if (data && typeof data === 'object') {
|
|
945
|
-
const inner = (data as { error?: unknown }).error;
|
|
946
|
-
if (typeof inner === 'string' && inner.length > 0) return inner;
|
|
947
|
-
}
|
|
948
|
-
if (typeof data === 'string' && data.length > 0) return data;
|
|
949
|
-
return err instanceof Error ? err.message : String(err);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
/**
|
|
953
|
-
* Turn a tool's final value into an MCP result:
|
|
954
|
-
* - a tool that already returns the MCP agentic shape (`{ content: [...] }`,
|
|
955
|
-
* each entry a real content block) is passed through unchanged;
|
|
956
|
-
* - a bare string becomes the text content;
|
|
957
|
-
* - anything else is JSON-stringified into one text block.
|
|
958
|
-
*
|
|
959
|
-
* Note we do NOT collapse a structured object down to its `text` field: doing
|
|
960
|
-
* so silently dropped the other fields (e.g. `ask`'s `status` / `sessionId`,
|
|
961
|
-
* the id a caller must echo back to poll or continue a conversation).
|
|
962
|
-
* Stringifying the whole object keeps every field, so the caller always sees
|
|
963
|
-
* the id it is responsible for echoing.
|
|
964
|
-
*
|
|
965
|
-
* The passthrough guard checks the entries, not just that `content` is an array:
|
|
966
|
-
* a domain value that merely happens to carry a `content` array of non-blocks
|
|
967
|
-
* (e.g. `{ content: ['a', 'b'] }`) is data, not an MCP result, so it falls
|
|
968
|
-
* through to JSON-stringify and survives intact instead of being emitted as a
|
|
969
|
-
* malformed result the client can't parse.
|
|
970
|
-
*/
|
|
971
|
-
function isMcpContentBlock(entry: unknown): boolean {
|
|
972
|
-
return typeof entry === 'object' && entry !== null && typeof (entry as { type?: unknown }).type === 'string';
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
export function toCallToolResult(value: unknown): CallToolResult {
|
|
976
|
-
// Already in MCP agentic format — pass through untouched, but only when every
|
|
977
|
-
// `content` entry is a real content block (has a string `type`).
|
|
978
|
-
const content = (value as { content?: unknown })?.content;
|
|
979
|
-
if (value && typeof value === 'object' && Array.isArray(content) && content.every(isMcpContentBlock)) {
|
|
980
|
-
return value as CallToolResult;
|
|
981
|
-
}
|
|
982
|
-
const text = typeof value === 'string' ? value : JSON.stringify(value ?? null);
|
|
983
|
-
return { content: [{ type: 'text', text: text || '(tool produced no output)' }] };
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function renderProgress(chunk: unknown): string {
|
|
987
|
-
const s = typeof chunk === 'string' ? chunk : JSON.stringify(chunk);
|
|
988
|
-
return s.length > 500 ? s.slice(0, 497) + '...' : s;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
function toolError(message: string): CallToolResult {
|
|
992
|
-
return { isError: true, content: [{ type: 'text', text: message }] };
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
/**
|
|
996
|
-
* The result returned when the caller is missing personal credentials a tool
|
|
997
|
-
* needs. Marked `isError` so the external agent surfaces it to the person rather
|
|
998
|
-
* than treating it as tool output. Names the tool, lists the missing items, and
|
|
999
|
-
* gives the absolute setup-page URL so the person can provide them and retry.
|
|
1000
|
-
*/
|
|
1001
|
-
export function needsAuthorizationResult(
|
|
1002
|
-
toolName: string,
|
|
1003
|
-
missing: string[],
|
|
1004
|
-
connectUrl: string,
|
|
1005
|
-
): CallToolResult {
|
|
1006
|
-
const items = missing.join(', ');
|
|
1007
|
-
const text =
|
|
1008
|
-
`The "${toolName}" tool needs credentials you haven't set up yet: ${items}. ` +
|
|
1009
|
-
`Open ${connectUrl} to connect your accounts and enter your keys, then run the tool again.`;
|
|
1010
|
-
return { isError: true, content: [{ type: 'text', text }] };
|
|
1011
|
-
}
|
|
624
|
+
export {
|
|
625
|
+
type ProxiedTool,
|
|
626
|
+
toListedTool,
|
|
627
|
+
sanitizeInputSchema,
|
|
628
|
+
flattenManualTool,
|
|
629
|
+
flattenDiscoveredTool,
|
|
630
|
+
describeToolFailure,
|
|
631
|
+
toCallToolResult,
|
|
632
|
+
needsAuthorizationResult,
|
|
633
|
+
} from '@bevel-software/platform-mcp-core';
|
|
@@ -5,7 +5,7 @@ import { pendingProposals } from '../join-proposals.js';
|
|
|
5
5
|
* What a join branch proposes, relative to the default branch. This diff IS
|
|
6
6
|
* the request's lifecycle: non-empty ⇒ open, empty ⇒ settled and closed.
|
|
7
7
|
*/
|
|
8
|
-
const PATH = '
|
|
8
|
+
const PATH = 'Plugins/GTM/access.md';
|
|
9
9
|
|
|
10
10
|
/** New-format access.md: `read: everyone` self-frontmatter, body = folder rules. */
|
|
11
11
|
const base = (body: string) => `---\nread:\n - everyone\n---\n${body}`;
|
|
@@ -18,7 +18,7 @@ import { JoinRequestsService } from '../join-requests.service.js';
|
|
|
18
18
|
* is what happens when that stops being true — nothing stores a status.
|
|
19
19
|
*/
|
|
20
20
|
const ALI = 'ali@bevel.software';
|
|
21
|
-
const FOLDER = '
|
|
21
|
+
const FOLDER = 'Plugins/GTM';
|
|
22
22
|
const ACCESS_MD = `${FOLDER}/access.md`;
|
|
23
23
|
const ACTOR: AuthUser = { id: 'u-1', email: 'olga@bevel.software', name: 'Olga Ivanova' };
|
|
24
24
|
|
|
@@ -132,21 +132,21 @@ describe('JoinRequestsService.list', () => {
|
|
|
132
132
|
warn.mockRestore();
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
it('NEVER settles a slug-colliding other
|
|
135
|
+
it('NEVER settles a slug-colliding other plugin\'s request (branch match is exact)', async () => {
|
|
136
136
|
// `Finance!` slugs to `finance` too. Its branch's copy of
|
|
137
|
-
// `
|
|
137
|
+
// `Plugins/finance/access.md` is unchanged, so if the branch matched, the
|
|
138
138
|
// empty diff would settle — closing a change request and deleting a
|
|
139
|
-
// branch that belong to a DIFFERENT
|
|
139
|
+
// branch that belong to a DIFFERENT plugin. The plugin tag in the branch
|
|
140
140
|
// name is what forbids the match.
|
|
141
141
|
const other = cr({ branch: joinBranchFor(ALI, 'Finance!') });
|
|
142
142
|
const h = makeHarness({
|
|
143
143
|
[`origin/${DEFAULT_BRANCH}`]: DEFAULT_MD,
|
|
144
144
|
[`origin/${joinBranchFor(ALI, 'Finance!')}`]: DEFAULT_MD,
|
|
145
145
|
});
|
|
146
|
-
await expect(h.svc.list('finance', '
|
|
146
|
+
await expect(h.svc.list('finance', 'Plugins/finance', [other], ACTOR)).resolves.toEqual([]);
|
|
147
147
|
expect(h.workflow.rejectChangeRequest).not.toHaveBeenCalled();
|
|
148
148
|
expect(h.workflow.deleteBranch).not.toHaveBeenCalled();
|
|
149
|
-
await expect(h.svc.reconcile('finance', '
|
|
149
|
+
await expect(h.svc.reconcile('finance', 'Plugins/finance', other, ACTOR)).resolves.toBe(false);
|
|
150
150
|
expect(h.workflow.rejectChangeRequest).not.toHaveBeenCalled();
|
|
151
151
|
});
|
|
152
152
|
});
|
|
@@ -165,7 +165,7 @@ describe('JoinRequestsService.reconcile', () => {
|
|
|
165
165
|
expect(h.workflow.rejectChangeRequest).not.toHaveBeenCalled();
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
-
it('refuses a change request that is not this
|
|
168
|
+
it('refuses a change request that is not this plugin\'s join branch', async () => {
|
|
169
169
|
const h = makeHarness(refs(PROPOSING_MD, PROPOSING_MD));
|
|
170
170
|
await expect(
|
|
171
171
|
h.svc.reconcile('GTM', FOLDER, cr({ branch: 'ali/some-draft' }), ACTOR),
|