@bevel-software/platform-core-backend 0.7.5 → 0.9.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 +8 -1
- 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 +75 -18
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +12 -5
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +69 -36
- 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.d.ts +3 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +22 -20
- 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/render-roles-yaml.d.ts +22 -0
- package/dist/modules/access/render-roles-yaml.d.ts.map +1 -0
- package/dist/modules/access/render-roles-yaml.js +56 -0
- package/dist/modules/access/render-roles-yaml.js.map +1 -0
- package/dist/modules/access/roles-admin.service.d.ts +33 -5
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +44 -19
- 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/settings/setup.routes.d.ts +10 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +111 -6
- package/dist/modules/settings/setup.routes.js.map +1 -1
- 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/startup/kb-git.d.ts +23 -0
- package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -0
- package/dist/modules/workspace/startup/kb-git.js +86 -0
- package/dist/modules/workspace/startup/kb-git.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +74 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.js +528 -0
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -0
- package/dist/modules/workspace/startup/on-server-start.d.ts +105 -0
- package/dist/modules/workspace/startup/on-server-start.d.ts.map +1 -0
- package/dist/modules/workspace/startup/on-server-start.js +21 -0
- package/dist/modules/workspace/startup/on-server-start.js.map +1 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +46 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +492 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +23 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js +69 -0
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts +17 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.js +109 -0
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -0
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +103 -0
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/template-files.step.js +337 -0
- package/dist/modules/workspace/startup/steps/template-files.step.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 +0 -9
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +2 -30
- 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/kb-template/gitignore.template +16 -0
- 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 +113 -106
- package/src/core/create-core-server.ts +85 -20
- package/src/core/create-core-services.ts +83 -42
- 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 +36 -2
- 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 +23 -20
- package/src/modules/access/creator-access.ts +5 -5
- package/src/modules/access/render-roles-yaml.ts +65 -0
- package/src/modules/access/roles-admin.service.ts +45 -16
- 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/settings/__tests__/setup.routes.test.ts +124 -3
- package/src/modules/settings/setup.routes.ts +112 -5
- 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__/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 +27 -0
- package/src/modules/workspace/bevel-ignore.ts +66 -66
- package/src/modules/workspace/session-sink.ts +25 -25
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +495 -0
- package/src/modules/workspace/startup/kb-git.ts +94 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +597 -0
- package/src/modules/workspace/startup/on-server-start.ts +97 -0
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +636 -0
- package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +561 -0
- package/src/modules/workspace/startup/steps/roles-yaml.step.ts +71 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +115 -0
- package/src/modules/workspace/startup/steps/template-files.step.ts +360 -0
- package/src/modules/workspace/workspace.routes.ts +3 -3
- package/src/modules/workspace/workspace.service.ts +2 -35
- 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/modules/workspace/__tests__/kb-seed.service.test.ts +0 -444
- package/src/modules/workspace/kb-seed.interface.ts +0 -36
- package/src/modules/workspace/kb-seed.service.ts +0 -555
- package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
- /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_BRANCH,
|
|
5
|
+
HEXIS_EXTENSION_NS,
|
|
6
|
+
PLUGINS_DIR,
|
|
7
|
+
PLUGIN_MANIFEST_FILE,
|
|
8
|
+
PLUGIN_MCP_FILE,
|
|
9
|
+
type AuthUser,
|
|
10
|
+
} from '@bevel-software/platform-shared';
|
|
11
|
+
import { workspaceIdForBranch, type WorkspaceService } from '../workspace/workspace.service.js';
|
|
12
|
+
import { validatedVariables } from './mcp-json-discovery.js';
|
|
13
|
+
import { assertSafeFetchUrl } from '../../shared/ssrf.js';
|
|
14
|
+
import { containsVariableReference, findReservedVariableRef } from '../../shared/variable-refs.js';
|
|
15
|
+
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
16
|
+
import type { IToolManualService, ToolVariable } from './tool-manuals.contract.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Server-scoped editing of one MCP server — the form the tool page uses
|
|
20
|
+
* instead of dropping a writer into raw `mcp.json`.
|
|
21
|
+
*
|
|
22
|
+
* One server's truth spans TWO files (the spec's split, not ours): the
|
|
23
|
+
* portable half in `mcp.json` (transport, url, literal headers) and our half
|
|
24
|
+
* in `plugin.json`'s `extensions["software.bevel.hexis"].mcpServers[<name>]`
|
|
25
|
+
* (auth headers carrying `${VAR}` vault references, variable declarations,
|
|
26
|
+
* description, `local`). This service is the ONE writer that keeps the two in
|
|
27
|
+
* step: a save rewrites both entries and commits them together, because a
|
|
28
|
+
* server whose auth landed without its url — or the reverse — is a broken
|
|
29
|
+
* tool with no author to blame.
|
|
30
|
+
*
|
|
31
|
+
* RENAME IS DESTRUCTIVE BY DESIGN and the route says so: the `mcpServers` key
|
|
32
|
+
* is the namespace vault secrets bind to (`<name>_<VAR>`), so renaming a
|
|
33
|
+
* server orphans every configured secret and completed sign-in under the old
|
|
34
|
+
* key. The frontend counts what disconnects (it already holds per-variable
|
|
35
|
+
* status) and confirms; this service just refuses a rename onto a taken key.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
export type McpTransport = 'streamable-http' | 'sse' | 'stdio';
|
|
39
|
+
|
|
40
|
+
export interface McpServerView {
|
|
41
|
+
name: string;
|
|
42
|
+
transport: McpTransport;
|
|
43
|
+
url?: string;
|
|
44
|
+
command?: string;
|
|
45
|
+
args?: string[];
|
|
46
|
+
cwd?: string;
|
|
47
|
+
env?: Record<string, string>;
|
|
48
|
+
/** Portable headers, stored in mcp.json. Never carry a `${VAR}`. */
|
|
49
|
+
literalHeaders: Record<string, string>;
|
|
50
|
+
/** Auth headers with vault references, stored in the extensions block. */
|
|
51
|
+
authHeaders: Record<string, string>;
|
|
52
|
+
variables: ToolVariable[];
|
|
53
|
+
description?: string;
|
|
54
|
+
local: boolean;
|
|
55
|
+
canWrite: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* PATCH semantics, field by field: `undefined` means "the client did not
|
|
60
|
+
* surface this field" and the stored value survives; a present-but-empty
|
|
61
|
+
* value (`{}`, `[]`, `''`) is an explicit clear. A full-replace contract
|
|
62
|
+
* would make every client responsible for echoing back fields it does not
|
|
63
|
+
* edit — and the one that forgot would silently destroy the literal headers
|
|
64
|
+
* in mcp.json or the variable declarations behind `${VAR}` references.
|
|
65
|
+
*/
|
|
66
|
+
export interface McpServerWrite {
|
|
67
|
+
newName?: string;
|
|
68
|
+
transport: McpTransport;
|
|
69
|
+
url?: string;
|
|
70
|
+
command?: string;
|
|
71
|
+
args?: string[];
|
|
72
|
+
cwd?: string;
|
|
73
|
+
env?: Record<string, string>;
|
|
74
|
+
literalHeaders?: Record<string, string>;
|
|
75
|
+
authHeaders?: Record<string, string>;
|
|
76
|
+
variables?: ToolVariable[];
|
|
77
|
+
description?: string;
|
|
78
|
+
local?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class McpServerEditError extends Error {
|
|
82
|
+
constructor(
|
|
83
|
+
message: string,
|
|
84
|
+
readonly status: number,
|
|
85
|
+
) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = 'McpServerEditError';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Same shape the discovery accepts: the key is a namespace, not prose. */
|
|
92
|
+
const SERVER_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
93
|
+
|
|
94
|
+
// The substitutor's own grammar (shared/variable-refs.ts) decides what is a
|
|
95
|
+
// reference: `${A-B}` is not (stays a literal header), `$5` is (digit-leading
|
|
96
|
+
// names are legal). The migration's header split applies the same predicate.
|
|
97
|
+
const hasVarRef = containsVariableReference;
|
|
98
|
+
|
|
99
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
100
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The same reserved-reference policy discovery enforces, applied at SAVE: a
|
|
105
|
+
* `${…API_URL}`/`${…CONNECTION_KEY}` persisted here would make discovery drop
|
|
106
|
+
* the server on its next scan — an editor that can save self-invalidating
|
|
107
|
+
* config is worse than a 422 naming the reference. The same shared grammar
|
|
108
|
+
* both sides scan with is what makes "saveable" and "discoverable" agree.
|
|
109
|
+
*/
|
|
110
|
+
const findReservedRef = findReservedVariableRef;
|
|
111
|
+
|
|
112
|
+
interface CommitDriver {
|
|
113
|
+
runPendingCommit(
|
|
114
|
+
workspaceId: string,
|
|
115
|
+
branch: string,
|
|
116
|
+
targetPath: string,
|
|
117
|
+
user: AuthUser,
|
|
118
|
+
opts?: { systemAuthorized?: boolean },
|
|
119
|
+
): Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class McpServerEditService {
|
|
123
|
+
constructor(
|
|
124
|
+
private readonly workspaceService: WorkspaceService,
|
|
125
|
+
private readonly commits: CommitDriver,
|
|
126
|
+
private readonly accessControl: IAccessControl,
|
|
127
|
+
private readonly toolManuals: IToolManualService,
|
|
128
|
+
private readonly kbDirName: string,
|
|
129
|
+
) {}
|
|
130
|
+
|
|
131
|
+
/** The merged view of one server, or null when unknown/unreadable (indistinguishable, fail closed). */
|
|
132
|
+
async getServer(userEmail: string, slug: string): Promise<McpServerView | null> {
|
|
133
|
+
const located = await this.locate(userEmail, slug);
|
|
134
|
+
if (!located) return null;
|
|
135
|
+
const { folder, name, wsId, mcpJsonPath } = located;
|
|
136
|
+
const { mcp, manifest } = await this.readFiles(folder);
|
|
137
|
+
const entry = (mcp?.mcpServers as Record<string, unknown> | undefined)?.[name];
|
|
138
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
139
|
+
const raw = entry as Record<string, unknown>;
|
|
140
|
+
const ext = this.extensionEntry(manifest, name);
|
|
141
|
+
return {
|
|
142
|
+
name,
|
|
143
|
+
transport: (raw.type as McpTransport) ?? 'streamable-http',
|
|
144
|
+
...(typeof raw.url === 'string' ? { url: raw.url } : {}),
|
|
145
|
+
...(typeof raw.command === 'string' ? { command: raw.command } : {}),
|
|
146
|
+
...(Array.isArray(raw.args) ? { args: raw.args.map(String) } : {}),
|
|
147
|
+
...(typeof raw.cwd === 'string' ? { cwd: raw.cwd } : {}),
|
|
148
|
+
...(raw.env && typeof raw.env === 'object' ? { env: raw.env as Record<string, string> } : {}),
|
|
149
|
+
// Shape-checked, not merely null-coalesced: both files are hand-editable
|
|
150
|
+
// knowledge-base content, and a string where an object/array belongs
|
|
151
|
+
// would flow into the form's Object.entries/`.map` as nonsense.
|
|
152
|
+
literalHeaders: isRecord(raw.headers) ? (raw.headers as Record<string, string>) : {},
|
|
153
|
+
authHeaders: isRecord(ext.headers) ? (ext.headers as Record<string, string>) : {},
|
|
154
|
+
variables: Array.isArray(ext.variables) ? (ext.variables as ToolVariable[]) : [],
|
|
155
|
+
...(typeof ext.description === 'string' ? { description: ext.description } : {}),
|
|
156
|
+
local: ext.local === true || raw.type === 'stdio',
|
|
157
|
+
canWrite: await this.accessControl.canWrite(wsId, userEmail, mcpJsonPath),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Rewrite one server across both files, in one commit. Returns the (possibly new) name. */
|
|
162
|
+
async putServer(user: AuthUser, slug: string, write: McpServerWrite): Promise<{ name: string }> {
|
|
163
|
+
const located = await this.locate(user.email, slug);
|
|
164
|
+
if (!located) throw new McpServerEditError('No such tool.', 404);
|
|
165
|
+
const { folder, name, wsId, mcpJsonPath } = located;
|
|
166
|
+
if (!(await this.accessControl.canWrite(wsId, user.email, mcpJsonPath))) {
|
|
167
|
+
throw new McpServerEditError("You don't have permission to edit this plugin's servers.", 403);
|
|
168
|
+
}
|
|
169
|
+
const { mcp, manifest, mcpAbs, manifestAbs } = await this.readFiles(folder);
|
|
170
|
+
if (!mcp?.mcpServers || typeof mcp.mcpServers !== 'object') {
|
|
171
|
+
throw new McpServerEditError(`${PLUGIN_MCP_FILE} is missing or unparsable — fix the file first.`, 422);
|
|
172
|
+
}
|
|
173
|
+
const servers = mcp.mcpServers as Record<string, unknown>;
|
|
174
|
+
if (!(name in servers)) throw new McpServerEditError('No such server.', 404);
|
|
175
|
+
|
|
176
|
+
if (write.transport !== 'streamable-http' && write.transport !== 'sse' && write.transport !== 'stdio') {
|
|
177
|
+
// Persisting an unknown transport would save an entry discovery then
|
|
178
|
+
// refuses — an unusable server with no error at the moment it was made.
|
|
179
|
+
throw new McpServerEditError(`Unknown transport "${String(write.transport)}".`, 422);
|
|
180
|
+
}
|
|
181
|
+
// The EFFECTIVE value of every field, PATCH-over-stored (see
|
|
182
|
+
// McpServerWrite): `undefined` keeps what the files already say, a
|
|
183
|
+
// present value — empty included — replaces it.
|
|
184
|
+
const prior = isRecord(servers[name]) ? (servers[name] as Record<string, unknown>) : {};
|
|
185
|
+
const priorExt = this.extensionEntry(manifest, name);
|
|
186
|
+
const url = write.url ?? (typeof prior.url === 'string' ? prior.url : undefined);
|
|
187
|
+
const command = write.command ?? (typeof prior.command === 'string' ? prior.command : undefined);
|
|
188
|
+
const args = write.args ?? (Array.isArray(prior.args) ? prior.args.map(String) : undefined);
|
|
189
|
+
const env = write.env ?? (isRecord(prior.env) ? (prior.env as Record<string, string>) : undefined);
|
|
190
|
+
const cwd = write.cwd ?? (typeof prior.cwd === 'string' ? prior.cwd : undefined);
|
|
191
|
+
const literalIn =
|
|
192
|
+
write.literalHeaders ?? (isRecord(prior.headers) ? (prior.headers as Record<string, string>) : {});
|
|
193
|
+
const authIn =
|
|
194
|
+
write.authHeaders ?? (isRecord(priorExt.headers) ? (priorExt.headers as Record<string, string>) : {});
|
|
195
|
+
// RAW on the stored side, no shape fallback: coercing malformed stored
|
|
196
|
+
// declarations to [] here would let a save that never touched variables
|
|
197
|
+
// silently CLEAR them — the validator below owes that case a 422, same
|
|
198
|
+
// as any other malformed input.
|
|
199
|
+
const variablesIn = write.variables ?? priorExt.variables;
|
|
200
|
+
// Discovery's OWN validator, at save time (like the reserved-ref check
|
|
201
|
+
// below): a malformed declaration — bad name, duplicate, re-declared
|
|
202
|
+
// platform name, oauth on a shared scope or with a broken provider —
|
|
203
|
+
// persisted here would make discovery drop the whole server on its next
|
|
204
|
+
// scan. What comes back is the normalized form discovery would read, and
|
|
205
|
+
// that is what gets stored.
|
|
206
|
+
const variables = validatedVariables(variablesIn);
|
|
207
|
+
if (variables === null) {
|
|
208
|
+
throw new McpServerEditError(
|
|
209
|
+
'The `variables` declaration is malformed — each entry needs a unique, non-reserved ' +
|
|
210
|
+
'alphanumeric/underscore name, a scope of `admin` or `user`, and any `oauth` block must sit ' +
|
|
211
|
+
'on a `user`-scoped variable with valid https provider URLs and a client id.',
|
|
212
|
+
422,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const description =
|
|
216
|
+
write.description ?? (typeof priorExt.description === 'string' ? priorExt.description : undefined);
|
|
217
|
+
const local = write.local ?? (priorExt.local === true);
|
|
218
|
+
|
|
219
|
+
// The WHOLE writable surface, mirroring what discovery scans — effective
|
|
220
|
+
// values, not just the incoming patch: a reserved ref smuggled through a
|
|
221
|
+
// stdio arg (`--url=${X_API_URL}`), env value, cwd or description would
|
|
222
|
+
// save with a 200 and then vanish from the catalog on the next scan —
|
|
223
|
+
// the exact self-invalidating state this 422 prevents.
|
|
224
|
+
const reservedRef = findReservedRef({
|
|
225
|
+
url,
|
|
226
|
+
command,
|
|
227
|
+
args,
|
|
228
|
+
env,
|
|
229
|
+
cwd,
|
|
230
|
+
description,
|
|
231
|
+
literalHeaders: literalIn,
|
|
232
|
+
authHeaders: authIn,
|
|
233
|
+
variables,
|
|
234
|
+
});
|
|
235
|
+
if (reservedRef !== null) {
|
|
236
|
+
throw new McpServerEditError(
|
|
237
|
+
`"${reservedRef}" references a platform-seeded variable (API_URL / CONNECTION_KEY) — ` +
|
|
238
|
+
'discovery refuses servers that name them, so this cannot be saved.',
|
|
239
|
+
422,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const target = write.newName?.trim() || name;
|
|
243
|
+
if (!SERVER_NAME_RE.test(target)) {
|
|
244
|
+
throw new McpServerEditError(
|
|
245
|
+
'A server name is its secret namespace: lowercase alphanumeric with `_`/`-`.',
|
|
246
|
+
422,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (target !== name && target in servers) {
|
|
250
|
+
throw new McpServerEditError(`A server named "${target}" already exists here.`, 409);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Defense in depth on the portable file: a `${VAR}` in mcp.json would be
|
|
254
|
+
// transmitted literally by a conformant client AND violate the spec's
|
|
255
|
+
// no-credentials rule — reroute it to the extensions block instead of
|
|
256
|
+
// trusting the frontend's split.
|
|
257
|
+
const literal: Record<string, string> = {};
|
|
258
|
+
const auth: Record<string, string> = { ...authIn };
|
|
259
|
+
for (const [k, v] of Object.entries(literalIn)) {
|
|
260
|
+
if (hasVarRef(v)) auth[k] = v;
|
|
261
|
+
else literal[k] = v;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let entry: Record<string, unknown>;
|
|
265
|
+
if (write.transport === 'stdio') {
|
|
266
|
+
const cmd = command?.trim();
|
|
267
|
+
if (!cmd) throw new McpServerEditError('A stdio server needs a command.', 422);
|
|
268
|
+
entry = {
|
|
269
|
+
type: 'stdio',
|
|
270
|
+
command: cmd,
|
|
271
|
+
...(args && args.length > 0 ? { args } : {}),
|
|
272
|
+
...(env && Object.keys(env).length > 0 ? { env } : {}),
|
|
273
|
+
...(cwd ? { cwd } : {}),
|
|
274
|
+
};
|
|
275
|
+
} else {
|
|
276
|
+
let parsed: URL;
|
|
277
|
+
try {
|
|
278
|
+
parsed = new URL(url ?? '');
|
|
279
|
+
} catch {
|
|
280
|
+
throw new McpServerEditError('The server needs a valid http(s) URL.', 422);
|
|
281
|
+
}
|
|
282
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
283
|
+
throw new McpServerEditError('The server needs a valid http(s) URL.', 422);
|
|
284
|
+
}
|
|
285
|
+
// The same SSRF gate discovery applies on read: without it here, a save
|
|
286
|
+
// succeeds and the server then silently disappears from the catalog —
|
|
287
|
+
// an edit flow that eats its own output. Local-only servers are exempt;
|
|
288
|
+
// loopback is what local means.
|
|
289
|
+
if (local !== true) {
|
|
290
|
+
try {
|
|
291
|
+
assertSafeFetchUrl(url ?? '', { label: 'Server URL' });
|
|
292
|
+
} catch (err) {
|
|
293
|
+
throw new McpServerEditError(
|
|
294
|
+
`${err instanceof Error ? err.message : 'The URL is not reachable from the workspace'} — ` +
|
|
295
|
+
'mark the server local-only if it is deliberately private.',
|
|
296
|
+
422,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
entry = {
|
|
301
|
+
type: write.transport,
|
|
302
|
+
url,
|
|
303
|
+
...(Object.keys(literal).length > 0 ? { headers: literal } : {}),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (target !== name) delete servers[name];
|
|
308
|
+
servers[target] = entry;
|
|
309
|
+
|
|
310
|
+
// The extensions half: rewrite our entry, drop the old key on rename.
|
|
311
|
+
// A missing/unparsable manifest refuses the save when there is auth to
|
|
312
|
+
// store — silently dropping a credential mapping is worse than an error.
|
|
313
|
+
const extEntry = {
|
|
314
|
+
...(Object.keys(auth).length > 0 ? { headers: auth } : {}),
|
|
315
|
+
...(variables.length > 0 ? { variables } : {}),
|
|
316
|
+
...(description ? { description } : {}),
|
|
317
|
+
...(local === true && write.transport !== 'stdio' ? { local: true } : {}),
|
|
318
|
+
};
|
|
319
|
+
if (manifest === null && Object.keys(extEntry).length > 0) {
|
|
320
|
+
throw new McpServerEditError(`${PLUGIN_MANIFEST_FILE} is missing or unparsable — fix the file first.`, 422);
|
|
321
|
+
}
|
|
322
|
+
if (manifest !== null) {
|
|
323
|
+
// A wrong-TYPED extensions chain (a string, an array) would throw a
|
|
324
|
+
// TypeError below and surface as a 500. It is also not ours to silently
|
|
325
|
+
// replace: an array `extensions` may be another tool's data, malformed
|
|
326
|
+
// or not, and a save aimed at one server should not discard it.
|
|
327
|
+
const badShape = (v: unknown): boolean => v !== undefined && (typeof v !== 'object' || v === null || Array.isArray(v));
|
|
328
|
+
const ext = (manifest.extensions ??= {}) as Record<string, unknown>;
|
|
329
|
+
if (badShape(manifest.extensions)) {
|
|
330
|
+
throw new McpServerEditError(`${PLUGIN_MANIFEST_FILE} has a malformed \`extensions\` block — fix the file first.`, 422);
|
|
331
|
+
}
|
|
332
|
+
if (badShape(ext[HEXIS_EXTENSION_NS]) || badShape((ext[HEXIS_EXTENSION_NS] as Record<string, unknown> | undefined)?.mcpServers)) {
|
|
333
|
+
throw new McpServerEditError(`${PLUGIN_MANIFEST_FILE} has a malformed \`extensions\` block — fix the file first.`, 422);
|
|
334
|
+
}
|
|
335
|
+
const ns = (ext[HEXIS_EXTENSION_NS] ??= {}) as Record<string, unknown>;
|
|
336
|
+
const extServers = (ns.mcpServers ??= {}) as Record<string, unknown>;
|
|
337
|
+
delete extServers[name];
|
|
338
|
+
if (Object.keys(extEntry).length > 0) extServers[target] = extEntry;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// All-or-nothing for real: snapshot both files first, and on ANY failure
|
|
342
|
+
// past the first write put the originals back — the API reporting failure
|
|
343
|
+
// while the workspace keeps half the edit is the state this exists to
|
|
344
|
+
// prevent.
|
|
345
|
+
// Rollback scope is the WRITES only. A failed write restores both files
|
|
346
|
+
// (a half-written pair is the state this exists to prevent); a failed
|
|
347
|
+
// COMMIT must not — the pending-commit pipeline may already have created
|
|
348
|
+
// a local commit before the push refused, and rewriting the working tree
|
|
349
|
+
// to pre-edit bytes would leave it dirty AGAINST that commit, a state the
|
|
350
|
+
// workflow layer's own pull-rebase recovery then misreads. Commit-stage
|
|
351
|
+
// failures propagate as-is and the pipeline's recovery owns the cleanup.
|
|
352
|
+
const [mcpBefore, manifestBefore] = await Promise.all([
|
|
353
|
+
fs.readFile(mcpAbs, 'utf8').catch(() => null),
|
|
354
|
+
fs.readFile(manifestAbs, 'utf8').catch(() => null),
|
|
355
|
+
]);
|
|
356
|
+
try {
|
|
357
|
+
await fs.writeFile(mcpAbs, `${JSON.stringify(mcp, null, 2)}\n`, 'utf8');
|
|
358
|
+
if (manifest !== null) {
|
|
359
|
+
await fs.writeFile(manifestAbs, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
if (mcpBefore !== null) await fs.writeFile(mcpAbs, mcpBefore, 'utf8').catch(() => {});
|
|
363
|
+
if (manifestBefore !== null) await fs.writeFile(manifestAbs, manifestBefore, 'utf8').catch(() => {});
|
|
364
|
+
throw err;
|
|
365
|
+
}
|
|
366
|
+
// One folder-scoped commit, ungated beyond the caller's own write access —
|
|
367
|
+
// both files or neither. The catalog cache is stale the moment it lands.
|
|
368
|
+
await this.commits.runPendingCommit(
|
|
369
|
+
workspaceIdForBranch(DEFAULT_BRANCH),
|
|
370
|
+
DEFAULT_BRANCH,
|
|
371
|
+
`${this.kbDirName}/${PLUGINS_DIR}/${folder}`,
|
|
372
|
+
user,
|
|
373
|
+
);
|
|
374
|
+
this.toolManuals.invalidate();
|
|
375
|
+
return { name: target };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Resolve a slug to an mcp.json-backed server the caller can READ; null otherwise. */
|
|
379
|
+
private async locate(
|
|
380
|
+
userEmail: string,
|
|
381
|
+
slug: string,
|
|
382
|
+
): Promise<{ folder: string; name: string; wsId: string; mcpJsonPath: string } | null> {
|
|
383
|
+
const summaries = await this.toolManuals.listAccessible(userEmail);
|
|
384
|
+
const found = summaries.find((s) => s.slug === slug);
|
|
385
|
+
if (!found || found.type !== 'mcp' || !found.path.endsWith(`/${PLUGIN_MCP_FILE}`)) return null;
|
|
386
|
+
const segments = found.path.split('/');
|
|
387
|
+
const folder = segments[1];
|
|
388
|
+
if (!folder) return null;
|
|
389
|
+
return {
|
|
390
|
+
folder,
|
|
391
|
+
name: found.name,
|
|
392
|
+
wsId: workspaceIdForBranch(DEFAULT_BRANCH),
|
|
393
|
+
mcpJsonPath: found.path,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private async readFiles(folder: string): Promise<{
|
|
398
|
+
mcp: Record<string, unknown> | null;
|
|
399
|
+
manifest: Record<string, unknown> | null;
|
|
400
|
+
mcpAbs: string;
|
|
401
|
+
manifestAbs: string;
|
|
402
|
+
}> {
|
|
403
|
+
const wsId = workspaceIdForBranch(DEFAULT_BRANCH);
|
|
404
|
+
await this.workspaceService.getOrCreateForBranch(DEFAULT_BRANCH);
|
|
405
|
+
const wsDir = await this.workspaceService.getWorkspacePath(wsId);
|
|
406
|
+
const pluginDir = path.join(wsDir, this.kbDirName, PLUGINS_DIR, folder);
|
|
407
|
+
const mcpAbs = path.join(pluginDir, PLUGIN_MCP_FILE);
|
|
408
|
+
const manifestAbs = path.join(pluginDir, PLUGIN_MANIFEST_FILE);
|
|
409
|
+
return {
|
|
410
|
+
mcp: await readJson(mcpAbs),
|
|
411
|
+
manifest: await readJson(manifestAbs),
|
|
412
|
+
mcpAbs,
|
|
413
|
+
manifestAbs,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private extensionEntry(manifest: Record<string, unknown> | null, name: string): Record<string, unknown> {
|
|
418
|
+
const ns = (manifest?.extensions as Record<string, unknown> | undefined)?.[HEXIS_EXTENSION_NS];
|
|
419
|
+
const servers = (ns as Record<string, unknown> | undefined)?.mcpServers;
|
|
420
|
+
const entry = (servers as Record<string, unknown> | undefined)?.[name];
|
|
421
|
+
return entry && typeof entry === 'object' ? (entry as Record<string, unknown>) : {};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function readJson(p: string): Promise<Record<string, unknown> | null> {
|
|
426
|
+
try {
|
|
427
|
+
const parsed: unknown = JSON.parse(await fs.readFile(p, 'utf-8'));
|
|
428
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
429
|
+
? (parsed as Record<string, unknown>)
|
|
430
|
+
: null;
|
|
431
|
+
} catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CallTemplate } from '@utcp/sdk';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Tool manuals — user-authored `*.tool` files under `
|
|
4
|
+
* Tool manuals — user-authored `*.tool` files under `Plugins/` in the DEFAULT
|
|
5
5
|
* branch KB. Each is a UTCP *manual* (a pointer to where tools come from), not a
|
|
6
6
|
* flattened tool list. Access-controlled exactly like Skills (default-deny ACL
|
|
7
7
|
* on the `.tool` file path). The MCP/UTCP endpoint serves every manual a user
|
|
@@ -85,8 +85,8 @@ export interface ToolManualSetup {
|
|
|
85
85
|
reason?: string;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
/**
|
|
89
|
-
export interface
|
|
88
|
+
/** Fields common to every parsed manual — see {@link ToolManualDescriptor}. */
|
|
89
|
+
export interface ToolManualDescriptorBase {
|
|
90
90
|
/** URL-safe id derived from the file name (route `:slug`), unique in the catalog. */
|
|
91
91
|
slug: string;
|
|
92
92
|
/**
|
|
@@ -95,7 +95,7 @@ export interface ToolManualDescriptor {
|
|
|
95
95
|
* catalog; namespacing doubles underscores in vault keys (`utcpNamespacedKey`).
|
|
96
96
|
*/
|
|
97
97
|
name: string;
|
|
98
|
-
/** Repo-root-relative path of the `.tool` file (e.g. `
|
|
98
|
+
/** Repo-root-relative path of the `.tool` file (e.g. `Plugins/Everyone/weather.tool`). */
|
|
99
99
|
path: string;
|
|
100
100
|
type: ToolManualType;
|
|
101
101
|
/**
|
|
@@ -113,17 +113,40 @@ export interface ToolManualDescriptor {
|
|
|
113
113
|
tools?: unknown[];
|
|
114
114
|
/** Declared `${VAR}` scopes (see {@link ToolVariable}); empty when none declared. */
|
|
115
115
|
variables?: ToolVariable[];
|
|
116
|
-
/**
|
|
117
|
-
* Whether this tool can run for a REMOTE consumer (Bevel's hosted MCP proxy).
|
|
118
|
-
* Absent/`true` ⇒ available remotely; `false` ⇒ LOCAL-ONLY — the remote endpoint
|
|
119
|
-
* skips it (it can't reach e.g. a `localhost` MCP server), and instead surfaces
|
|
120
|
-
* its path via the `list_local_tools` tool so a local agent can self-configure it.
|
|
121
|
-
*/
|
|
122
|
-
remote?: boolean;
|
|
123
116
|
/** For `type: mcp`: the admin-facing setup requirement from auto-discovery. */
|
|
124
117
|
setup?: ToolManualSetup;
|
|
125
118
|
}
|
|
126
119
|
|
|
120
|
+
/** The spawn spec of a stdio-declared MCP server (a plugin `mcp.json` entry). */
|
|
121
|
+
export interface ToolManualStdioSpec {
|
|
122
|
+
command: string;
|
|
123
|
+
args: string[];
|
|
124
|
+
env?: Record<string, string>;
|
|
125
|
+
cwd?: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A parsed manual (a `.tool` file or a plugin `mcp.json` entry, normalized).
|
|
130
|
+
*
|
|
131
|
+
* `remote` — whether the tool can run for a REMOTE consumer (Bevel's hosted
|
|
132
|
+
* MCP proxy). Absent/`true` ⇒ available remotely; `false` ⇒ LOCAL-ONLY — the
|
|
133
|
+
* remote endpoint skips it (it can't reach e.g. a `localhost` MCP server),
|
|
134
|
+
* and instead surfaces its path via the `list_local_tools` tool so a local
|
|
135
|
+
* agent can self-configure it.
|
|
136
|
+
*
|
|
137
|
+
* `stdio` — for a `type: mcp` server declared with a stdio transport in a
|
|
138
|
+
* plugin's `mcp.json`: the spawn spec. A UNION, not two optional fields,
|
|
139
|
+
* because a stdio spec REQUIRES `remote: false` — the hosted proxy can never
|
|
140
|
+
* spawn a subprocess out of knowledge-base content, so stdio servers are
|
|
141
|
+
* served only to local consumers, which run them per the Agent Plugins
|
|
142
|
+
* runtime contract (PLUGIN_ROOT/PLUGIN_DATA, `./` containment) — and the
|
|
143
|
+
* type refusing `remote: true` beside a spawn spec is what keeps every
|
|
144
|
+
* producer honest about that.
|
|
145
|
+
*/
|
|
146
|
+
export type ToolManualDescriptor =
|
|
147
|
+
| (ToolManualDescriptorBase & { remote?: boolean; stdio?: undefined })
|
|
148
|
+
| (ToolManualDescriptorBase & { type: 'mcp'; remote: false; stdio: ToolManualStdioSpec });
|
|
149
|
+
|
|
127
150
|
/** A validated UTCP manual dict (`{ utcp_version, manual_version, tools }`). */
|
|
128
151
|
export type UtcpManualDict = Record<string, unknown>;
|
|
129
152
|
|
|
@@ -188,7 +211,7 @@ export interface IToolManualService {
|
|
|
188
211
|
/**
|
|
189
212
|
* Every manual in the catalog, UNFILTERED by access — the mirror of
|
|
190
213
|
* `skillService.listSkills(undefined)`. For caller-INDEPENDENT counting only
|
|
191
|
-
* (the
|
|
214
|
+
* (the plugin index's "N tools", which a non-member is allowed to see as a
|
|
192
215
|
* number). Never surface a name, path or description from this to someone
|
|
193
216
|
* who cannot read the file; `listAccessible` is the surface for that.
|
|
194
217
|
*/
|