@stage-labs/metro 0.1.0-beta.304 → 0.1.0-beta.306
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/dist/cli.js +1 -1
- package/dist/memory.js +2 -3
- package/dist/runtime-install.js +3 -11
- package/dist/voice.js +1 -1
- package/{runtime/marketplace/plugin/bin → hooks}/guard.mjs +9 -4
- package/{runtime/marketplace/plugin/bin → hooks}/policy.mjs +1 -32
- package/{runtime/marketplace/plugin/bin → hooks}/session-start.mjs +2 -2
- package/package.json +4 -2
- package/runtime/node_modules/@metro-labs/core/package.json +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/home-fs.ts +2 -29
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +2 -15
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +3 -5
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +1 -7
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +16 -17
- package/runtime/node_modules/@metro-labs/daemon/src/claude/cli-files.ts +28 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/hooks.ts +60 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-cleanup.ts +121 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +2 -4
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +38 -30
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/enroll-api.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +6 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +4 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +1 -13
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +2 -1
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +2 -16
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/server-approvals.ts +15 -16
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +4 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +20 -8
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +14 -8
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/announced.ts +68 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/config.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/connector-tools.ts +114 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/link.ts +12 -1
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/rpc.ts +67 -0
- package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +6 -5
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +2 -27
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +0 -5
- package/runtime/node_modules/@metro-labs/http/package.json +3 -3
- package/runtime/node_modules/@metro-labs/http/src/connector-grant.ts +0 -51
- package/runtime/node_modules/@metro-labs/http/src/mcp-server-protocol.ts +9 -4
- package/runtime/node_modules/@metro-labs/http/src/mcp-server-tools.ts +93 -0
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/package.json +0 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/{runtime/marketplace/plugin → skills}/METRO.md +8 -3
- package/runtime/marketplace/.claude-plugin/marketplace.json +0 -12
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +0 -17
- package/runtime/marketplace/plugin/.mcp.json +0 -1
- package/runtime/marketplace/plugin/bin/metro-plugin.mjs +0 -48
- package/runtime/marketplace/plugin/hooks/hooks.json +0 -25
- package/runtime/node_modules/@metro-labs/core/src/connector-sync.ts +0 -17
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +0 -149
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +0 -104
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-adopt.ts +0 -57
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-relay.ts +0 -61
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-runtime-discovery.ts +0 -115
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-runtime.ts +0 -271
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-api.ts +0 -25
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-control.ts +0 -173
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-setting.ts +0 -60
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-watch.ts +0 -38
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync.ts +0 -208
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +0 -145
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/publication.ts +0 -40
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-id.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-policy.ts +0 -90
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-session.ts +0 -10
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +0 -4
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +0 -287
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/server-keys.ts +0 -59
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +0 -1
- package/runtime/node_modules/@metro-labs/http/src/connector-delivery-validation.ts +0 -116
- package/runtime/node_modules/@metro-labs/http/src/connector-delivery.ts +0 -19
- package/runtime/node_modules/@metro-labs/http/src/connector-migration.ts +0 -178
- package/runtime/node_modules/@metro-labs/http/src/transfer-crypto.ts +0 -44
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/connector-sync.ts +0 -17
- /package/{runtime/marketplace/plugin → skills}/MEMORY.md +0 -0
- /package/{runtime/marketplace/plugin → skills}/STAGE.md +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
5
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
6
|
+
import { existsSync } from '../agent-user/agent-fs.js';
|
|
7
|
+
import { removeHome } from '../agent-user/home-fs.js';
|
|
8
|
+
import { agentUser, asAgent, claudeBin } from '../agent-user/user.js';
|
|
9
|
+
import { agentsDir } from '../agents/files.js';
|
|
10
|
+
import { readSetupState, writeSetupState } from './setup-state.js';
|
|
11
|
+
|
|
12
|
+
const PLUGIN = 'metro@metro';
|
|
13
|
+
const MARKETPLACE = 'metro';
|
|
14
|
+
const DONE = 'pluginRemoved';
|
|
15
|
+
const STEP_MS = 60_000;
|
|
16
|
+
const SAID_MAX = 300;
|
|
17
|
+
|
|
18
|
+
export interface Answer {
|
|
19
|
+
status: number;
|
|
20
|
+
stdout: string;
|
|
21
|
+
stderr: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Run = (args: string[]) => Promise<Answer>;
|
|
25
|
+
|
|
26
|
+
export interface CleanupDeps {
|
|
27
|
+
run?: Run | null;
|
|
28
|
+
agents?: string;
|
|
29
|
+
leftover?: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type CleanupOutcome = 'done' | 'clean' | 'removed' | 'skipped' | 'failed';
|
|
33
|
+
|
|
34
|
+
export const claudeAt =
|
|
35
|
+
(bin: string): Run =>
|
|
36
|
+
(args) =>
|
|
37
|
+
new Promise((resolve) => {
|
|
38
|
+
execFile(...asAgent(bin, args), { encoding: 'utf8', timeout: STEP_MS, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
39
|
+
const code = (err as { code?: unknown } | null)?.code;
|
|
40
|
+
resolve({ status: err === null ? 0 : typeof code === 'number' ? code : 1, stdout, stderr });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
async function findClaude(): Promise<Run | null> {
|
|
45
|
+
const candidates = agentUser() === null ? ['claude', join(homedir(), '.local', 'bin', 'claude')] : [claudeBin()];
|
|
46
|
+
for (const bin of candidates) {
|
|
47
|
+
const run = claudeAt(bin);
|
|
48
|
+
if ((await run(['--version'])).status === 0) return run;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listOf(stdout: string): Record<string, unknown>[] {
|
|
54
|
+
try {
|
|
55
|
+
const parsed: unknown = JSON.parse(stdout);
|
|
56
|
+
return Array.isArray(parsed) ? parsed.filter(isRecord) : [];
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function listed(run: Run, args: string[], key: string, wanted: string): Promise<boolean | null> {
|
|
63
|
+
const answer = await run(args);
|
|
64
|
+
if (answer.status !== 0) {
|
|
65
|
+
log.warn({ args, said: `${answer.stderr}${answer.stdout}`.trim().slice(0, SAID_MAX) }, 'claude-plugin: claude could not list');
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return listOf(answer.stdout).some((entry) => entry[key] === wanted);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function step(run: Run, args: string[]): Promise<boolean> {
|
|
72
|
+
const answer = await run(args);
|
|
73
|
+
if (answer.status === 0) {
|
|
74
|
+
log.info({ args }, 'claude-plugin: removed what the old metro plugin left');
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
log.warn({ args, said: `${answer.stderr}${answer.stdout}`.trim().slice(0, SAID_MAX) }, 'claude-plugin: claude refused');
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function removeIfListed(run: Run, list: string[], key: string, wanted: string, remove: string[]): Promise<'gone' | 'removed' | 'failed'> {
|
|
82
|
+
const present = await listed(run, list, key, wanted);
|
|
83
|
+
if (present === null) return 'failed';
|
|
84
|
+
if (!present) return 'gone';
|
|
85
|
+
return (await step(run, remove)) ? 'removed' : 'failed';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function leftoverDir(): string | null {
|
|
89
|
+
const user = agentUser();
|
|
90
|
+
return user === null ? null : join(user.home, '.metro', 'marketplace');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function removeLeftover(dir: string | null): boolean {
|
|
94
|
+
if (dir === null || !existsSync(dir)) return true;
|
|
95
|
+
try {
|
|
96
|
+
removeHome(dir, true);
|
|
97
|
+
log.info({ dir }, 'claude-plugin: removed the old plugin marketplace copy');
|
|
98
|
+
return true;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
log.warn({ dir, err: errMsg(err) }, 'claude-plugin: could not remove the old plugin marketplace copy');
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function cleanUp(run: Run, leftover: string | null): Promise<'removed' | 'clean' | 'failed'> {
|
|
106
|
+
const plugin = await removeIfListed(run, ['plugin', 'list', '--json'], 'id', PLUGIN, ['plugin', 'uninstall', PLUGIN]);
|
|
107
|
+
const market = await removeIfListed(run, ['plugin', 'marketplace', 'list', '--json'], 'name', MARKETPLACE, ['plugin', 'marketplace', 'remove', MARKETPLACE]);
|
|
108
|
+
const folder = removeLeftover(leftover);
|
|
109
|
+
if (plugin === 'failed' || market === 'failed' || !folder) return 'failed';
|
|
110
|
+
return plugin === 'removed' || market === 'removed' ? 'removed' : 'clean';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function removeMetroPlugin(deps: CleanupDeps = {}): Promise<CleanupOutcome> {
|
|
114
|
+
const agents = deps.agents ?? agentsDir();
|
|
115
|
+
if (readSetupState(agents)[DONE] === true) return 'done';
|
|
116
|
+
const run = deps.run === undefined ? await findClaude() : deps.run;
|
|
117
|
+
if (run === null) return 'skipped';
|
|
118
|
+
const outcome = await cleanUp(run, deps.leftover === undefined ? leftoverDir() : deps.leftover);
|
|
119
|
+
if (outcome !== 'failed') writeSetupState(agents, { [DONE]: true });
|
|
120
|
+
return outcome;
|
|
121
|
+
}
|
|
@@ -49,7 +49,6 @@ export interface SessionDeps {
|
|
|
49
49
|
now?: () => number;
|
|
50
50
|
version?: string;
|
|
51
51
|
prepare?: (runner: HarnessRunner) => Promise<void>;
|
|
52
|
-
connectorsReady?: () => boolean;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
export interface SessionStatus {
|
|
@@ -317,10 +316,9 @@ function strike(now: number): boolean {
|
|
|
317
316
|
return true;
|
|
318
317
|
}
|
|
319
318
|
|
|
320
|
-
export type Ensured = 'started' | 'restarted' | 'running' | 'blocked' | 'off' | 'paused'
|
|
319
|
+
export type Ensured = 'started' | 'restarted' | 'running' | 'blocked' | 'off' | 'paused';
|
|
321
320
|
|
|
322
321
|
function holdsBack(deps: SessionDeps, now: number): Ensured | null {
|
|
323
|
-
if (deps.connectorsReady?.() === false) return 'waiting';
|
|
324
322
|
if (now < memory.pausedUntil) return 'paused';
|
|
325
323
|
if (sessionBlocked(deps) !== null) return 'blocked';
|
|
326
324
|
return strike(now) ? 'paused' : null;
|
|
@@ -354,7 +352,7 @@ export function ensureSession(deps: SessionDeps = {}): Ensured {
|
|
|
354
352
|
const held = holdsBack(deps, r.now);
|
|
355
353
|
if (held !== null) return held;
|
|
356
354
|
if (found === 'stale') {
|
|
357
|
-
log.info({ was: startedVersion(r.agents), now: r.version }, 'claude-session: restarting the session an older metro started, so the new
|
|
355
|
+
log.info({ was: startedVersion(r.agents), now: r.version }, 'claude-session: restarting the session an older metro started, so the new hooks and flags load');
|
|
358
356
|
stopSession(deps);
|
|
359
357
|
}
|
|
360
358
|
startSession(deps);
|
|
@@ -2,7 +2,6 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { rmSync } from 'node:fs';
|
|
3
3
|
import { existsSync, readFileSync, statSync } from '../agent-user/agent-fs.js';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
7
6
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
8
7
|
import { writeAtomic } from '@metro-labs/core/secure-fs';
|
|
@@ -10,7 +9,8 @@ import { agentsDir } from '../agents/files.js';
|
|
|
10
9
|
import { moveHome, removeHome, writeHomeText } from '../agent-user/home-fs.js';
|
|
11
10
|
import { claudeDir } from './files.js';
|
|
12
11
|
import { memoryJobStatus, type MemoryJob } from './memory-routine.js';
|
|
13
|
-
import {
|
|
12
|
+
import { cliPackageDir, hooksDir, skillsDir } from './cli-files.js';
|
|
13
|
+
import { hasMetroGuard, withMetroHooks } from './hooks.js';
|
|
14
14
|
import { readModelConfig, routedConnection, type ModelConfig } from '../gateway/model-config.js';
|
|
15
15
|
import { harnessRunner, runnerModel, sdkAllowed, sdkOnLogin, type HarnessRunner } from './runner.js';
|
|
16
16
|
import { readSetupState, writeSetupState } from './setup-state.js';
|
|
@@ -49,7 +49,7 @@ Your final message is a report to the orchestrator, not a message to a human. It
|
|
|
49
49
|
export interface SetupDeps {
|
|
50
50
|
dir?: string;
|
|
51
51
|
agents?: string;
|
|
52
|
-
|
|
52
|
+
cli?: string;
|
|
53
53
|
env?: NodeJS.ProcessEnv;
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -58,7 +58,7 @@ export type SettingsOutcome = 'written' | 'unchanged' | 'unreadable';
|
|
|
58
58
|
|
|
59
59
|
export interface SetupReport {
|
|
60
60
|
privacy: boolean;
|
|
61
|
-
guard:
|
|
61
|
+
guard: boolean;
|
|
62
62
|
worker: Placed;
|
|
63
63
|
skill: Placed;
|
|
64
64
|
stage: Placed;
|
|
@@ -76,7 +76,7 @@ export interface SetupStatus {
|
|
|
76
76
|
liveEvents: boolean;
|
|
77
77
|
memoryRoutine: boolean;
|
|
78
78
|
memoryJob: MemoryJob;
|
|
79
|
-
guard:
|
|
79
|
+
guard: boolean;
|
|
80
80
|
worker: boolean;
|
|
81
81
|
skill: boolean;
|
|
82
82
|
stage: boolean;
|
|
@@ -134,15 +134,6 @@ export function setSystemPrompt(text: string, agents = agentsDir()): void {
|
|
|
134
134
|
writeAtomic(promptPath(agents), `${trimmed}\n`, 0o600);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
function pluginDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
138
|
-
const staged = stagedMarketplaceDir(env);
|
|
139
|
-
if (staged !== null) {
|
|
140
|
-
const dir = join(staged, 'plugin');
|
|
141
|
-
if (existsSync(join(dir, RULES_FILE))) return dir;
|
|
142
|
-
}
|
|
143
|
-
return fileURLToPath(new URL('../../../../plugin', import.meta.url));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
137
|
const PRIOR_WORKER: ReadonlySet<string> = new Set(['08a0cd8710df285d6512245246bb8b658b7cfc7f3e89490514ee5376779eb2ef']);
|
|
147
138
|
const PRIOR_METRO: ReadonlySet<string> = new Set([
|
|
148
139
|
'0b3d122ed95beff09d579cf912cd4238e1db524c41fce4b314de57d6ff5908ad',
|
|
@@ -152,6 +143,9 @@ const PRIOR_METRO: ReadonlySet<string> = new Set([
|
|
|
152
143
|
'5ce3f76d610aae91adc922d0edc6c8be1f27aa2fec4a5e3a6d6231a04fb9310b',
|
|
153
144
|
'b910597d24f31fa479b130a2545f94dd23684d802d99e36b637fc57c818a33be',
|
|
154
145
|
'd3f7ed0f5175b9a7a77f06f6204ae0020369d0c8f6e2724196bb3c5822d4d8ba',
|
|
146
|
+
'587a1334f91a8b61541573cef16eaf44fbe63049a8d8b461fa9f5cc55bacbcb8',
|
|
147
|
+
'256c711a6175266c216e32c88b7190d0f98f5e6b702abe03220b422aba426574',
|
|
148
|
+
'9e164a956437d60d30ea1984115a17d303940d191f5efed04954f244fc89d48f',
|
|
155
149
|
]);
|
|
156
150
|
const PRIOR_STAGE: ReadonlySet<string> = new Set([
|
|
157
151
|
'dd7d30474765de3865ddc87c1cf3220161ded09502027cdff18597e5df0aab43',
|
|
@@ -219,15 +213,29 @@ function mergeSettings(dir: string, change: (settings: Record<string, unknown>)
|
|
|
219
213
|
return 'written';
|
|
220
214
|
}
|
|
221
215
|
|
|
222
|
-
|
|
223
|
-
|
|
216
|
+
interface SettingsApplied {
|
|
217
|
+
settings: SettingsOutcome;
|
|
218
|
+
guard: boolean;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function applySettings(dir: string, privacy: boolean, hooks: string | null): SettingsApplied {
|
|
222
|
+
let guard = false;
|
|
223
|
+
const settings = mergeSettings(dir, (current) => {
|
|
224
|
+
const next = withPrivacy(current, privacy);
|
|
225
|
+
const hooked = hooks === null ? null : withMetroHooks(next, hooks);
|
|
226
|
+
if (hooks !== null && hooked === null)
|
|
227
|
+
log.warn({ path: join(dir, 'settings.json') }, 'claude-setup: the hooks in settings.json are not in the shape Claude Code reads, so metro did not add its own');
|
|
228
|
+
guard = hasMetroGuard(hooked ?? next);
|
|
229
|
+
return hooked ?? next;
|
|
230
|
+
});
|
|
231
|
+
return { settings, guard };
|
|
224
232
|
}
|
|
225
233
|
|
|
226
234
|
const skillPath = (dir: string, name: string): string => join(dir, 'skills', name, 'SKILL.md');
|
|
227
235
|
const workerPath = (dir: string): string => join(dir, 'agents', 'worker.md');
|
|
228
236
|
|
|
229
|
-
function placeSkill(dir: string,
|
|
230
|
-
const source = join(
|
|
237
|
+
function placeSkill(dir: string, skills: string, skill: ShippedSkill): Placed {
|
|
238
|
+
const source = join(skills, skill.file);
|
|
231
239
|
if (!existsSync(source)) return 'missing';
|
|
232
240
|
return placeFile(skillPath(dir, skill.name), readFileSync(source, 'utf8'), skill.prior);
|
|
233
241
|
}
|
|
@@ -264,21 +272,21 @@ export function ensureClaudeSetup(deps: SetupDeps = {}): SetupReport {
|
|
|
264
272
|
const dir = deps.dir ?? claudeDir();
|
|
265
273
|
const agents = deps.agents ?? agentsDir();
|
|
266
274
|
const privacy = privacyEnabled(agents);
|
|
267
|
-
const
|
|
275
|
+
const cli = deps.cli ?? cliPackageDir(deps.env);
|
|
276
|
+
const hooks = hooksDir(cli);
|
|
268
277
|
tryMoveRenamedSkill(dir);
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
settings: applyPrivacy(dir, privacy),
|
|
277
|
-
};
|
|
278
|
+
const worker = placeFile(workerPath(dir), WORKER_AGENT, PRIOR_WORKER);
|
|
279
|
+
const skills = skillsDir(cli);
|
|
280
|
+
const skill = placeSkill(dir, skills, METRO_SKILL);
|
|
281
|
+
const stage = placeSkill(dir, skills, STAGE_SKILL);
|
|
282
|
+
const memory = placeSkill(dir, skills, MEMORY_SKILL);
|
|
283
|
+
const applied = applySettings(dir, privacy, hooks);
|
|
284
|
+
const report: SetupReport = { privacy, guard: applied.guard, worker, skill, stage, memory, settings: applied.settings };
|
|
278
285
|
syncAvailableModelsQuietly({ ...deps, dir, agents });
|
|
279
286
|
if ([report.worker, report.skill, report.stage, report.memory].some(written) || report.settings === 'written')
|
|
280
287
|
log.info(report, 'claude-setup: applied the Claude Code setup for a metro box');
|
|
281
|
-
if (report.settings === 'unreadable') log.warn({ path: join(dir, 'settings.json') }, 'claude-setup: settings.json is not valid JSON, so the privacy settings were not written');
|
|
288
|
+
if (report.settings === 'unreadable') log.warn({ path: join(dir, 'settings.json') }, 'claude-setup: settings.json is not valid JSON, so the privacy settings and metro hooks were not written');
|
|
289
|
+
if (hooks === null) log.warn({ cli }, 'claude-setup: metro hooks were not found next to the CLI, so the guard and the standing rules are not in settings.json');
|
|
282
290
|
return report;
|
|
283
291
|
}
|
|
284
292
|
|
|
@@ -298,7 +306,7 @@ export function claudeSetupStatus(deps: SetupDeps = {}): SetupStatus {
|
|
|
298
306
|
liveEvents: liveEvents(agents),
|
|
299
307
|
memoryRoutine: memoryRoutine(agents),
|
|
300
308
|
memoryJob: memoryJobStatus(),
|
|
301
|
-
guard:
|
|
309
|
+
guard: hasMetroGuard(settings),
|
|
302
310
|
worker: existsSync(workerPath(dir)),
|
|
303
311
|
skill: existsSync(skillPath(dir, METRO_SKILL.name)),
|
|
304
312
|
stage: existsSync(skillPath(dir, STAGE_SKILL.name)),
|
|
@@ -18,7 +18,6 @@ export interface EnrollApiDeps {
|
|
|
18
18
|
owner: () => string | null;
|
|
19
19
|
dir?: () => string;
|
|
20
20
|
api?: MetroApi;
|
|
21
|
-
invalidate?: () => void;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
export interface Enrolled {
|
|
@@ -89,7 +88,6 @@ async function enroll(req: IncomingMessage, session: ApiSession, deps: EnrollApi
|
|
|
89
88
|
if (enrolling.has(dir)) throw new ApiError('This agent is already enrolling. Wait for it to finish before trying again.', 409);
|
|
90
89
|
enrolling.add(dir);
|
|
91
90
|
try {
|
|
92
|
-
deps.invalidate?.();
|
|
93
91
|
return await exchange(ticket, owner, dir, deps);
|
|
94
92
|
} finally {
|
|
95
93
|
enrolling.delete(dir);
|
|
@@ -21,6 +21,7 @@ import { SessionSlot } from './session-slot.js';
|
|
|
21
21
|
import { connectCallBridge } from './call-bridge.js';
|
|
22
22
|
import { connectServerApprovals } from './server-approvals.js';
|
|
23
23
|
import { handleIncomingDiagnostic } from './diagnostics.js';
|
|
24
|
+
import { onConnectorToolsChanged } from '../mcp-server/connector-tools.js';
|
|
24
25
|
|
|
25
26
|
const isInitialize = (b: unknown): boolean =>
|
|
26
27
|
!!b &&
|
|
@@ -99,6 +100,7 @@ async function serveGet(
|
|
|
99
100
|
const isVoicePath = (req: IncomingMessage): boolean => (req.url ?? '').split('?')[0] === VOICE_MCP_PATH;
|
|
100
101
|
|
|
101
102
|
let activeSlots: SessionSlot[] = [];
|
|
103
|
+
let stopToolNotices: (() => void) | undefined;
|
|
102
104
|
|
|
103
105
|
const MODEL_NOTICE = 'notifications/metro/model';
|
|
104
106
|
|
|
@@ -120,6 +122,10 @@ export async function createMetroMcp(options: { liveEvents?: boolean } = {}): Pr
|
|
|
120
122
|
const slot = new SessionSlot(options.liveEvents);
|
|
121
123
|
const voiceSlot = new SessionSlot(false, 'voice');
|
|
122
124
|
activeSlots = [slot, voiceSlot];
|
|
125
|
+
stopToolNotices?.();
|
|
126
|
+
stopToolNotices = onConnectorToolsChanged(() => {
|
|
127
|
+
for (const each of [slot, voiceSlot]) each.current?.toolsChanged();
|
|
128
|
+
});
|
|
123
129
|
connectCallBridge(() => slot.current);
|
|
124
130
|
connectServerApprovals(() => slot.current);
|
|
125
131
|
|
|
@@ -16,7 +16,9 @@ export const MCP_INSTRUCTIONS =
|
|
|
16
16
|
'it need approval (`list_accounts` shows each `policy`). A blocked call errors. A call ' +
|
|
17
17
|
'needing approval must run in a background worker: on the main thread it is refused, so ' +
|
|
18
18
|
'delegate that exact call and keep answering; the worker waits for the owner. Approval ' +
|
|
19
|
-
'prompts are relayed to the chat; the owner answers "yes <id>"/"no <id>".'
|
|
19
|
+
'prompts are relayed to the chat; the owner answers "yes <id>"/"no <id>". Connectors ' +
|
|
20
|
+
'(third-party tools like Notion, n8n, Gmail) are reached only through `tool_search` then ' +
|
|
21
|
+
'`tool_call`, from a worker.';
|
|
20
22
|
|
|
21
23
|
export const VOICE_MCP_INSTRUCTIONS =
|
|
22
24
|
"Metro's chat tools, opened for a live voice call. The caller hears what you say, so do not " +
|
|
@@ -27,4 +29,4 @@ export const VOICE_MCP_INSTRUCTIONS =
|
|
|
27
29
|
'(`list_accounts` shows each `policy`). A blocked call errors. A call needing approval must ' +
|
|
28
30
|
'run in a background worker: on the main thread it is refused, so delegate that exact call ' +
|
|
29
31
|
'and keep talking; the worker waits while the owner is asked in the chat of the call, where ' +
|
|
30
|
-
'they answer "yes <id>"/"no <id>".';
|
|
32
|
+
'they answer "yes <id>"/"no <id>". Connectors are reached through `tool_search` then `tool_call`.';
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { str } from '@metro-labs/core/str';
|
|
2
2
|
import { readPreview, shownValue } from '../approvals/preview.js';
|
|
3
|
-
import { connectorGates, connectorToolOf } from '../connectors/gates.js';
|
|
4
3
|
import { stationByName } from '../stations/registry.js';
|
|
5
4
|
import { stationOfAccount } from './read-tool.js';
|
|
6
5
|
|
|
@@ -83,21 +82,11 @@ function argLines(input: Record<string, unknown> | undefined): string[] {
|
|
|
83
82
|
return args.length > ARGS_MAX ? [...args.slice(0, ARGS_MAX), `(${String(args.length - ARGS_MAX)} more)`] : args;
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
function connectorLines(name: string, tool: string, input: Record<string, unknown> | undefined): string[] {
|
|
87
|
-
return [`Approval needed: ${tool}`, `Connector: ${name}`, ...argLines(input)];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
85
|
function bodyLines(params: PermissionParams): string[] {
|
|
91
86
|
const metro = METRO_TOOL.exec(params.tool_name)?.[1];
|
|
92
87
|
const { input } = readPreview(params.input_preview);
|
|
93
88
|
const raw = params.input_preview.trim() === '' ? [] : [shorten(params.input_preview, RAW_MAX)];
|
|
94
89
|
if (metro !== undefined) return input === undefined ? [`Approval needed: ${metro}`, ...raw] : metroLines(metro, input);
|
|
95
|
-
const connector = connectorToolOf(params.tool_name);
|
|
96
|
-
if (connector !== undefined) {
|
|
97
|
-
const { gate, tool } = connector;
|
|
98
|
-
const duplicate = connectorGates().some((entry) => entry.id !== gate.id && entry.name === gate.name);
|
|
99
|
-
return connectorLines(duplicate ? `${gate.name} (${gate.id})` : gate.name, tool, input);
|
|
100
|
-
}
|
|
101
90
|
return [`Claude wants to run ${params.tool_name}: ${shorten(params.description)}`, '', ...raw];
|
|
102
91
|
}
|
|
103
92
|
|
|
@@ -115,7 +104,6 @@ export function promptBody(params: PermissionParams): string {
|
|
|
115
104
|
export interface ServerPrompt {
|
|
116
105
|
requestId: string;
|
|
117
106
|
tool: string;
|
|
118
|
-
connector: string | undefined;
|
|
119
107
|
preview: string;
|
|
120
108
|
}
|
|
121
109
|
|
|
@@ -123,5 +111,5 @@ export function serverPromptBody(prompt: ServerPrompt): string {
|
|
|
123
111
|
const { input } = readPreview(prompt.preview);
|
|
124
112
|
const raw = prompt.preview.trim() === '' ? [] : [shorten(prompt.preview, RAW_MAX)];
|
|
125
113
|
const args = input === undefined ? raw : argLines(input);
|
|
126
|
-
return withAnswer(prompt.requestId, [`Approval needed: ${prompt.tool}`, ...
|
|
114
|
+
return withAnswer(prompt.requestId, [`Approval needed: ${prompt.tool}`, ...args]);
|
|
127
115
|
}
|
|
@@ -7,10 +7,11 @@ import { NEEDS_APPROVAL, takeGrant } from '../approvals/pending.js';
|
|
|
7
7
|
import { profileScopeLine } from './profile-lookup.js';
|
|
8
8
|
import { stationOfAccount } from './read-tool.js';
|
|
9
9
|
import { stationForTool, toolGroupOf } from './tool-catalog.js';
|
|
10
|
+
import { CONNECTOR_TOOLS } from '../mcp-server/connector-tools.js';
|
|
10
11
|
|
|
11
12
|
type ChannelTarget = Extract<PolicyTarget, { kind: 'channel' }>;
|
|
12
13
|
|
|
13
|
-
export const UNGATED = new Set<string>(['list_accounts', 'create_upload']);
|
|
14
|
+
export const UNGATED = new Set<string>(['list_accounts', 'create_upload', ...CONNECTOR_TOOLS]);
|
|
14
15
|
|
|
15
16
|
function fromLine(line: string, override = ''): ChannelTarget[] {
|
|
16
17
|
const acct = accountFromLine(line);
|
|
@@ -7,7 +7,6 @@ import { agentsDir } from '../agents/files.js';
|
|
|
7
7
|
import { knownAccounts } from '../agents/map.js';
|
|
8
8
|
import { onPoliciesChanged, storedPolicies, type ToolPolicy } from '../policy/policy.js';
|
|
9
9
|
import { UNGATED } from './policy-gate.js';
|
|
10
|
-
import { connectorGates } from '../connectors/gates.js';
|
|
11
10
|
import { stationToolOwners, TOOL_DEFS, toolGroupOf } from './tool-catalog.js';
|
|
12
11
|
|
|
13
12
|
interface PolicySnapshot {
|
|
@@ -17,14 +16,6 @@ interface PolicySnapshot {
|
|
|
17
16
|
ungated: string[];
|
|
18
17
|
accounts: Record<string, ToolPolicy>;
|
|
19
18
|
stations: Record<string, string[]>;
|
|
20
|
-
connectors: Record<string, SnapshotConnector>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface SnapshotConnector {
|
|
24
|
-
id: string;
|
|
25
|
-
name: string;
|
|
26
|
-
policy: ToolPolicy;
|
|
27
|
-
tools: Record<string, ToolGroup>;
|
|
28
19
|
}
|
|
29
20
|
|
|
30
21
|
const FILE = 'policy.json';
|
|
@@ -44,11 +35,6 @@ function policySnapshot(): PolicySnapshot {
|
|
|
44
35
|
ungated: [...UNGATED],
|
|
45
36
|
accounts,
|
|
46
37
|
stations,
|
|
47
|
-
connectors: Object.fromEntries(
|
|
48
|
-
connectorGates()
|
|
49
|
-
.filter((gate) => gate.server !== '')
|
|
50
|
-
.map((gate) => [gate.server, { id: gate.id, name: gate.name, policy: gate.policy, tools: gate.tools }]),
|
|
51
|
-
),
|
|
52
38
|
};
|
|
53
39
|
}
|
|
54
40
|
|
|
@@ -65,9 +51,9 @@ function writePolicySnapshot(path = policySnapshotPath()): void {
|
|
|
65
51
|
if (current(path) === text) return;
|
|
66
52
|
try {
|
|
67
53
|
writeSecure(path, text);
|
|
68
|
-
log.info({ path }, 'policy: snapshot for the
|
|
54
|
+
log.info({ path }, 'policy: snapshot for the guard hook written');
|
|
69
55
|
} catch (err) {
|
|
70
|
-
log.warn({ path, err: errMsg(err) }, 'policy: the snapshot for the
|
|
56
|
+
log.warn({ path, err: errMsg(err) }, 'policy: the snapshot for the guard hook could not be written');
|
|
71
57
|
}
|
|
72
58
|
}
|
|
73
59
|
|
|
@@ -4,8 +4,7 @@ import type { McpApproval, McpDecision } from '@metro-labs/http/mcp-server-proto
|
|
|
4
4
|
import { holdPrompt, isPromptHeld, type Behavior } from '../approvals/pending.js';
|
|
5
5
|
import { ownerLine } from '../approvals/owner-line.js';
|
|
6
6
|
import { metroNotice } from '../channels/notice.js';
|
|
7
|
-
import {
|
|
8
|
-
import { serverKey } from '../connectors/plugin-sync.js';
|
|
7
|
+
import { announcedLine, noteAnnounced } from '../mcp-server/announced.js';
|
|
9
8
|
import type { AnswerOutcome } from '../mcp-server/link.js';
|
|
10
9
|
import { serverPromptBody } from './permission-prompt.js';
|
|
11
10
|
import { metroSend, relayLine } from './permission-relay.js';
|
|
@@ -42,13 +41,8 @@ function remember(id: string, now = Date.now()): void {
|
|
|
42
41
|
|
|
43
42
|
const answeredRecently = (id: string, now = Date.now()): boolean => now - (recent.get(id) ?? -Infinity) < RECENT_MS;
|
|
44
43
|
|
|
45
|
-
function connectorName(key: string): string | undefined {
|
|
46
|
-
const names = new Set(connectorGates().filter((gate) => gate.server === key || serverKey(gate.name) === key).map((gate) => gate.name));
|
|
47
|
-
return names.size === 1 ? [...names][0] : undefined;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
44
|
const noticeText = (tool: string, id: string, behavior: Behavior): string =>
|
|
51
|
-
behavior === 'allow' ? `Approved: ${tool} (${id}). Call
|
|
45
|
+
behavior === 'allow' ? `Approved: ${tool} (${id}). Call tool_call again with the same arguments and approval: "${id}".` : `Denied: ${tool} (${id}).`;
|
|
52
46
|
|
|
53
47
|
const requestedAt = (at: number, now = Date.now()): number => (Number.isFinite(at) && at > 0 ? Math.min(at, now) : now);
|
|
54
48
|
|
|
@@ -84,23 +78,28 @@ function sender(held: Held): (behavior: Behavior) => Promise<void> {
|
|
|
84
78
|
};
|
|
85
79
|
}
|
|
86
80
|
|
|
81
|
+
function announce(id: string, line: string, text: string): void {
|
|
82
|
+
metroSend(line, text).then(() => {
|
|
83
|
+
noteAnnounced(id, line);
|
|
84
|
+
}).catch((err: unknown) => {
|
|
85
|
+
log.warn({ id, err: errMsg(err) }, 'mcp-server: the approval prompt could not be posted');
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
87
89
|
export function holdServerApproval(approval: McpApproval, answer: Held['answer'], publish: Held['publish']): boolean {
|
|
88
90
|
if (isPromptHeld(approval.id) || answeredRecently(approval.id)) return false;
|
|
89
91
|
const tool = `${approval.connector}.${approval.tool}`;
|
|
90
|
-
const
|
|
91
|
-
const line = promptLineFor(approval.id);
|
|
92
|
+
const announced = announcedLine(approval.id);
|
|
93
|
+
const line = announced ?? promptLineFor(approval.id);
|
|
92
94
|
const held: Held = { id: approval.id, tool, line, answer, publish };
|
|
93
95
|
holdPrompt(
|
|
94
|
-
{ requestId: approval.id, tool, description:
|
|
96
|
+
{ requestId: approval.id, tool, description: approval.connector, preview: approval.preview, line, at: requestedAt(approval.requestedAt), origin: 'mcp-server' },
|
|
95
97
|
owner,
|
|
96
98
|
sender(held),
|
|
97
99
|
line === undefined ? undefined : async (text) => metroSend(line, text),
|
|
98
100
|
);
|
|
99
|
-
log.info({ id: approval.id, tool, inChat: line !== undefined }, 'mcp-server: an approval is waiting for the owner');
|
|
100
|
-
if (line !== undefined)
|
|
101
|
-
metroSend(line, serverPromptBody({ requestId: approval.id, tool, connector: name, preview: approval.preview })).catch((err: unknown) => {
|
|
102
|
-
log.warn({ id: approval.id, err: errMsg(err) }, 'mcp-server: the approval prompt could not be posted');
|
|
103
|
-
});
|
|
101
|
+
log.info({ id: approval.id, tool, inChat: line !== undefined, announced: announced !== undefined }, 'mcp-server: an approval is waiting for the owner');
|
|
102
|
+
if (line !== undefined && announced === undefined) announce(approval.id, line, serverPromptBody({ requestId: approval.id, tool, preview: approval.preview }));
|
|
104
103
|
return true;
|
|
105
104
|
}
|
|
106
105
|
|
|
@@ -227,6 +227,10 @@ export class McpSession {
|
|
|
227
227
|
return this.issuedSchema !== toolSchemaSignature();
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
toolsChanged(): void {
|
|
231
|
+
this.announceToolSchema();
|
|
232
|
+
}
|
|
233
|
+
|
|
230
234
|
private announceToolSchema(): void {
|
|
231
235
|
if (!this.streamAttached) return;
|
|
232
236
|
this.deliverSchemaNotice(
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { GroupOp, Station, StationTool, ToolGroup } from '@metro-labs/core/stations/types';
|
|
2
|
+
import { TOOL_CALL, TOOL_SEARCH } from '@metro-labs/http/mcp-server-protocol';
|
|
3
|
+
import { connectorTools } from '../mcp-server/connector-tools.js';
|
|
2
4
|
import { STATIONS } from '../stations/registry.js';
|
|
3
5
|
import { COMMON_TOOLS, LIST_ACCOUNTS_TOOL } from './tool-schemas.js';
|
|
4
6
|
import { SET_PROFILE_TOOL } from './profile-tool.js';
|
|
@@ -14,7 +16,7 @@ const defOf = (tool: StationTool): ToolDef => ({
|
|
|
14
16
|
...(tool.destructive === true ? { destructive: true } : {}),
|
|
15
17
|
});
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
const LOCAL_DEFS: ToolDef[] = [
|
|
18
20
|
...COMMON_TOOLS,
|
|
19
21
|
...STATIONS.flatMap((s) => s.tools.map(defOf)),
|
|
20
22
|
LIST_ACCOUNTS_TOOL,
|
|
@@ -23,19 +25,27 @@ export const TOOL_DEFS: ToolDef[] = [
|
|
|
23
25
|
GET_PROFILE_TOOL,
|
|
24
26
|
];
|
|
25
27
|
|
|
28
|
+
const CONNECTOR_GROUPS: Record<string, Pick<ToolDef, 'group' | 'destructive'>> = {
|
|
29
|
+
[TOOL_SEARCH]: { group: 'read' },
|
|
30
|
+
[TOOL_CALL]: { group: 'write', destructive: true },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const TOOL_DEFS: ToolDef[] = [
|
|
34
|
+
...LOCAL_DEFS,
|
|
35
|
+
...connectorTools().map((tool) => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, ...CONNECTOR_GROUPS[tool.name] })),
|
|
36
|
+
];
|
|
37
|
+
|
|
26
38
|
const BY_NAME = new Map(TOOL_DEFS.map((d) => [d.name, d] as const));
|
|
27
39
|
|
|
28
40
|
export const declaredGroup = (name: string): ToolGroup | undefined => BY_NAME.get(name)?.group;
|
|
29
41
|
|
|
30
42
|
export const toolGroupOf = (name: string): ToolGroup => declaredGroup(name) ?? 'write';
|
|
31
43
|
|
|
32
|
-
export
|
|
33
|
-
readOnlyHint: boolean;
|
|
34
|
-
destructiveHint?: boolean;
|
|
35
|
-
}
|
|
44
|
+
export type ToolAnnotations = Record<string, unknown>;
|
|
36
45
|
|
|
37
46
|
export interface PublishedTool {
|
|
38
47
|
name: string;
|
|
48
|
+
title?: string;
|
|
39
49
|
description: string;
|
|
40
50
|
inputSchema: Record<string, unknown>;
|
|
41
51
|
annotations: ToolAnnotations;
|
|
@@ -50,14 +60,16 @@ const annotationsOf = (def: ToolDef): ToolAnnotations =>
|
|
|
50
60
|
? { readOnlyHint: true }
|
|
51
61
|
: { readOnlyHint: false, destructiveHint: def.destructive === true };
|
|
52
62
|
|
|
53
|
-
export const listedTools = (): PublishedTool[] =>
|
|
54
|
-
|
|
63
|
+
export const listedTools = (): PublishedTool[] => [
|
|
64
|
+
...LOCAL_DEFS.map((def) => ({
|
|
55
65
|
name: def.name,
|
|
56
66
|
description: def.description,
|
|
57
67
|
inputSchema: def.inputSchema,
|
|
58
68
|
annotations: annotationsOf(def),
|
|
59
69
|
...(ALWAYS_LOADED.has(def.name) ? { _meta: ALWAYS_LOAD } : {}),
|
|
60
|
-
}))
|
|
70
|
+
})),
|
|
71
|
+
...connectorTools().map((tool) => ({ ...tool, _meta: ALWAYS_LOAD })),
|
|
72
|
+
];
|
|
61
73
|
|
|
62
74
|
const STATION_OF_TOOL = new Map(STATIONS.flatMap((s) => s.tools.map((t) => [t.name, s.name] as const)));
|
|
63
75
|
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import { str } from '@metro-labs/core/str';
|
|
39
39
|
import { linelessRead, stationOfAccount } from './read-tool.js';
|
|
40
40
|
import { channelArgsError, channelScopeDenied, dispatchListChannels } from './channel-tool.js';
|
|
41
|
+
import { callConnectorTool, CONNECTOR_TOOLS, connectorToolsVersion, type ConnectorResult } from '../mcp-server/connector-tools.js';
|
|
41
42
|
|
|
42
43
|
const STATION_TOOLS = new Map<
|
|
43
44
|
string,
|
|
@@ -63,14 +64,13 @@ const CORE_DISPATCH: Record<
|
|
|
63
64
|
|
|
64
65
|
export const toolList = (): { tools: PublishedTool[] } => ({ tools: listedTools() });
|
|
65
66
|
|
|
66
|
-
let schemaSignature: string | undefined;
|
|
67
|
+
let schemaSignature: { version: number; value: string } | undefined;
|
|
67
68
|
|
|
68
69
|
export const toolSchemaSignature = (): string => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.digest('hex')
|
|
72
|
-
|
|
73
|
-
return schemaSignature;
|
|
70
|
+
const version = connectorToolsVersion();
|
|
71
|
+
if (schemaSignature?.version !== version)
|
|
72
|
+
schemaSignature = { version, value: createHash('sha256').update(JSON.stringify(toolList())).digest('hex').slice(0, 16) };
|
|
73
|
+
return schemaSignature.value;
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
async function handleListAccounts(
|
|
@@ -197,6 +197,12 @@ export async function callToolHandler(
|
|
|
197
197
|
return result;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
function connectorToolHandler(req: { params: { name: string; arguments?: Record<string, unknown> } }): Promise<ConnectorResult> {
|
|
201
|
+
const args = req.params.arguments ?? {};
|
|
202
|
+
settlePromptsFor(req.params.name, args);
|
|
203
|
+
return callConnectorTool(req.params.name, args);
|
|
204
|
+
}
|
|
205
|
+
|
|
200
206
|
const TOOL_LIST_CHANGED = 'notifications/tools/list_changed';
|
|
201
207
|
|
|
202
208
|
export interface SchemaNotice {
|
|
@@ -222,11 +228,11 @@ export function registerToolHandlers(
|
|
|
222
228
|
((
|
|
223
229
|
req: { params: { name: string; arguments?: Record<string, unknown> } },
|
|
224
230
|
extra: NotifyingExtra,
|
|
225
|
-
): Promise<ToolResult> => {
|
|
231
|
+
): Promise<ToolResult | ConnectorResult> => {
|
|
226
232
|
notice?.deliver(() =>
|
|
227
233
|
extra.sendNotification({ method: TOOL_LIST_CHANGED }),
|
|
228
234
|
);
|
|
229
|
-
return callToolHandler(req, hooks);
|
|
235
|
+
return CONNECTOR_TOOLS.has(req.params.name) ? connectorToolHandler(req) : callToolHandler(req, hooks);
|
|
230
236
|
}) as Parameters<typeof server.setRequestHandler>[1],
|
|
231
237
|
);
|
|
232
238
|
}
|