@stage-labs/metro 0.1.0-beta.317 → 0.1.0-beta.318

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.317",
3
+ "version": "0.1.0-beta.318",
4
4
  "description": "The metro command line: run your agent on this machine, relaying its chat channels and connectors to Claude Code.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,10 +46,16 @@ const noticeText = (tool: string, id: string, behavior: Behavior): string =>
46
46
 
47
47
  const requestedAt = (at: number, now = Date.now()): number => (Number.isFinite(at) && at > 0 ? Math.min(at, now) : now);
48
48
 
49
+ const askedBy = (approval: McpApproval): string => {
50
+ if (approval.origin !== 'key') return '';
51
+ return approval.keyLabel === undefined ? ' (from an access key)' : ` (from an access key: ${approval.keyLabel})`;
52
+ };
53
+
49
54
  interface Held {
50
55
  id: string;
51
56
  tool: string;
52
57
  line: string | undefined;
58
+ tellAgent: boolean;
53
59
  answer: (decision: McpDecision) => Promise<AnswerOutcome>;
54
60
  publish: (event: MetroEvent) => void;
55
61
  }
@@ -60,6 +66,10 @@ async function deliver(held: Held, behavior: Behavior): Promise<void> {
60
66
  log.info({ id: held.id, outcome }, 'mcp-server: the answer was not taken, so the agent is not told');
61
67
  return;
62
68
  }
69
+ if (!held.tellAgent) {
70
+ log.info({ id: held.id }, 'mcp-server: the call came with an access key, not from this agent\'s session, so the agent is not told');
71
+ return;
72
+ }
63
73
  const line = noticeLineFor(held.line);
64
74
  if (line === undefined) {
65
75
  log.warn({ id: held.id }, 'mcp-server: no chat is known to tell the agent the answer');
@@ -91,15 +101,15 @@ export function holdServerApproval(approval: McpApproval, answer: Held['answer']
91
101
  const tool = `${approval.connector}.${approval.tool}`;
92
102
  const announced = announcedLine(approval.id);
93
103
  const line = announced ?? promptLineFor(approval.id);
94
- const held: Held = { id: approval.id, tool, line, answer, publish };
104
+ const held: Held = { id: approval.id, tool, line, tellAgent: approval.origin !== 'key', answer, publish };
95
105
  holdPrompt(
96
106
  { requestId: approval.id, tool, description: approval.connector, preview: approval.preview, line, at: requestedAt(approval.requestedAt), origin: 'mcp-server' },
97
107
  owner,
98
108
  sender(held),
99
109
  line === undefined ? undefined : async (text) => metroSend(line, text),
100
110
  );
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 }));
111
+ log.info({ id: approval.id, tool, origin: approval.origin ?? null, inChat: line !== undefined, announced: announced !== undefined }, 'mcp-server: an approval is waiting for the owner');
112
+ if (line !== undefined && announced === undefined) announce(approval.id, line, serverPromptBody({ requestId: approval.id, tool: `${tool}${askedBy(approval)}`, preview: approval.preview }));
103
113
  return true;
104
114
  }
105
115
 
@@ -27,12 +27,18 @@ export interface McpCall {
27
27
  approval?: string;
28
28
  }
29
29
 
30
+ type McpApprovalOrigin = 'box' | 'key';
31
+
32
+ const KEY_LABEL_MAX = 64;
33
+
30
34
  export interface McpApproval {
31
35
  id: string;
32
36
  connector: string;
33
37
  tool: string;
34
38
  preview: string;
35
39
  requestedAt: number;
40
+ origin?: McpApprovalOrigin;
41
+ keyLabel?: string;
36
42
  }
37
43
 
38
44
  export type McpDecision = 'allow' | 'deny';
@@ -70,9 +76,16 @@ export function mcpTokenHashOf(raw: unknown): string {
70
76
  return raw.tokenHash;
71
77
  }
72
78
 
79
+ function originOf(raw: Record<string, unknown>): Pick<McpApproval, 'origin' | 'keyLabel'> {
80
+ if (raw.origin === 'box') return { origin: 'box' };
81
+ if (raw.origin !== 'key') return {};
82
+ const label = typeof raw.keyLabel === 'string' ? raw.keyLabel.replace(/\p{Cc}/gu, '').trim() : '';
83
+ return label === '' || label.length > KEY_LABEL_MAX ? { origin: 'key' } : { origin: 'key', keyLabel: label };
84
+ }
85
+
73
86
  function approvalOf(raw: unknown): McpApproval | null {
74
87
  if (!isRecord(raw) || typeof raw.id !== 'string' || !MCP_APPROVAL_ID_RE.test(raw.id) || typeof raw.connector !== 'string' || typeof raw.tool !== 'string' || typeof raw.preview !== 'string' || typeof raw.requestedAt !== 'number') return null;
75
- return { id: raw.id, connector: raw.connector, tool: raw.tool, preview: raw.preview, requestedAt: raw.requestedAt };
88
+ return { id: raw.id, connector: raw.connector, tool: raw.tool, preview: raw.preview, requestedAt: raw.requestedAt, ...originOf(raw) };
76
89
  }
77
90
 
78
91
  export function mcpApprovalsOf(raw: unknown): McpApproval[] {
@@ -61,19 +61,30 @@ export const toolSearchDescription = (keys: string[] | null): string =>
61
61
  'Results are full definitions (name, description and the JSON schema of the arguments) in a <functions> block. Each name is <connector>.<tool>: pass it exactly as "tool" to tool_call. Tools the owner blocked are never returned.',
62
62
  ].join('\n');
63
63
 
64
- export const TOOL_CALL_DESCRIPTION = [
65
- 'Run connector tools that tool_search found. Always find a tool with tool_search first: its exact name and argument schema come from there.',
66
- `Shape: ${TOOL_CALL_EXAMPLE}`,
67
- `Up to ${String(MCP_CALLS_MAX)} independent calls run in parallel in one tool_call and are answered in order. Each result starts with "[n] <connector>.<tool>", then the tool's own content, or one line saying why it did not run:`,
68
- '- not_found: no such connector or tool here. Run tool_search, then use a name it returned.',
69
- '- needs_approval: the owner was asked in chat and on metro.box. Tell the person what is waiting, wait for the channel message "Approved: <tool> (<id>)", then call again with the same tool and arguments plus "approval": "<id>". An approval works once.',
70
- '- denied or blocked: the owner refused or blocked this call. Do not retry it.',
71
- '- signin: the connector must be signed in again. Tell the owner to do it on the organization\'s Connectors page on metro.box.',
72
- '- unavailable: the connector or Metro could not be reached. Try again later.',
73
- '- vendor_error: the connector answered with its own error, shown as it came.',
74
- ].join('\n');
64
+ export type ApprovalPlace = 'session' | 'page' | 'owner';
75
65
 
76
- export function mcpServerTools(keys: string[] | null): McpServerTool[] {
66
+ const NEEDS_APPROVAL: Record<ApprovalPlace, string> = {
67
+ session: '- needs_approval: the owner was asked in chat and on metro.box. Tell the person what is waiting, wait for the channel message "Approved: <tool> (<id>)", then call again with the same tool and arguments plus "approval": "<id>". An approval works once.',
68
+ page: '- needs_approval: the owner must approve it on this agent\'s page on metro.box. Tell the person what is waiting and ask them to approve it there, then call again with the same tool and arguments plus "approval": "<id>". An approval works once, within 10 minutes of the answer.',
69
+ owner: '- needs_approval: the agent\'s owner was asked in chat and on its page on metro.box. Tell the person what is waiting; once it is approved, call again with the same tool and arguments plus "approval": "<id>". An approval works once, within 10 minutes of the answer.',
70
+ };
71
+
72
+ const toolCallDescription = (place: ApprovalPlace): string =>
73
+ [
74
+ 'Run connector tools that tool_search found. Always find a tool with tool_search first: its exact name and argument schema come from there.',
75
+ `Shape: ${TOOL_CALL_EXAMPLE}`,
76
+ `Up to ${String(MCP_CALLS_MAX)} independent calls run in parallel in one tool_call and are answered in order. Each result starts with "[n] <connector>.<tool>", then the tool's own content, or one line saying why it did not run:`,
77
+ '- not_found: no such connector or tool here. Run tool_search, then use a name it returned.',
78
+ NEEDS_APPROVAL[place],
79
+ '- denied or blocked: the owner refused or blocked this call. Do not retry it.',
80
+ '- signin: the connector must be signed in again. Tell the owner to do it on the organization\'s Connectors page on metro.box.',
81
+ '- unavailable: the connector or Metro could not be reached. Try again later.',
82
+ '- vendor_error: the connector answered with its own error, shown as it came.',
83
+ ].join('\n');
84
+
85
+ export const TOOL_CALL_DESCRIPTION = toolCallDescription('session');
86
+
87
+ export function mcpServerTools(keys: string[] | null, place: ApprovalPlace = 'session'): McpServerTool[] {
77
88
  return [
78
89
  {
79
90
  name: TOOL_SEARCH,
@@ -85,7 +96,7 @@ export function mcpServerTools(keys: string[] | null): McpServerTool[] {
85
96
  {
86
97
  name: TOOL_CALL,
87
98
  title: 'Call connector tools',
88
- description: TOOL_CALL_DESCRIPTION,
99
+ description: toolCallDescription(place),
89
100
  inputSchema: callSchema,
90
101
  annotations: { title: 'Call connector tools', readOnlyHint: false, destructiveHint: true, openWorldHint: true },
91
102
  },
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.317"
2
+ "version": "0.1.0-beta.318"
3
3
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-beta.317",
2
+ "version": "0.1.0-beta.318",
3
3
  "dependencies": {
4
4
  "@anthropic-ai/claude-agent-sdk": "0.3.287",
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",