agent-relay-server 0.62.3 → 0.63.1

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.
Files changed (181) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +4 -2
  3. package/public/assets/{MessageBubble-DRrxfjyT.js → MessageBubble-Cl6ufPHw.js} +2 -2
  4. package/public/assets/MessageBubble-Cl6ufPHw.js.map +1 -0
  5. package/public/assets/{PlanGraphPanel-Dp2eYazz.js → PlanGraphPanel-BnfjmkQ8.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-Dp2eYazz.js.map → PlanGraphPanel-BnfjmkQ8.js.map} +1 -1
  7. package/public/assets/{activity--t0U2yzq.js → activity-aFxvGLtd.js} +2 -2
  8. package/public/assets/{activity--t0U2yzq.js.map → activity-aFxvGLtd.js.map} +1 -1
  9. package/public/assets/{agent-profiles-BMtJYzxn.js → agent-profiles-B4mleUDz.js} +2 -2
  10. package/public/assets/{agent-profiles-BMtJYzxn.js.map → agent-profiles-B4mleUDz.js.map} +1 -1
  11. package/public/assets/{agents-Ck9AePlc.js → agents-DW5djToQ.js} +2 -2
  12. package/public/assets/{agents-Ck9AePlc.js.map → agents-DW5djToQ.js.map} +1 -1
  13. package/public/assets/{analytics-CzfK387L.js → analytics-CRi6thtg.js} +2 -2
  14. package/public/assets/{analytics-CzfK387L.js.map → analytics-CRi6thtg.js.map} +1 -1
  15. package/public/assets/{automation-Cgw8vtE8.js → automation-Do4OpauI.js} +2 -2
  16. package/public/assets/{automation-Cgw8vtE8.js.map → automation-Do4OpauI.js.map} +1 -1
  17. package/public/assets/{branch-state-badge-DnEEPtOP.js → branch-state-badge-D3W3w7zz.js} +2 -2
  18. package/public/assets/{branch-state-badge-DnEEPtOP.js.map → branch-state-badge-D3W3w7zz.js.map} +1 -1
  19. package/public/assets/{channels-Dc3L2MQs.js → channels-BytdR9NS.js} +2 -2
  20. package/public/assets/{channels-Dc3L2MQs.js.map → channels-BytdR9NS.js.map} +1 -1
  21. package/public/assets/{chat-Fc6iJ7sa.js → chat-CvaQ2czL.js} +2 -2
  22. package/public/assets/{chat-Fc6iJ7sa.js.map → chat-CvaQ2czL.js.map} +1 -1
  23. package/public/assets/{connectors-Dc2ttGWs.js → connectors-BLQDp8Bc.js} +2 -2
  24. package/public/assets/{connectors-Dc2ttGWs.js.map → connectors-BLQDp8Bc.js.map} +1 -1
  25. package/public/assets/{display-DzNvOu1j.js → display-B06I5sqa.js} +3 -3
  26. package/public/assets/display-B06I5sqa.js.map +1 -0
  27. package/public/assets/{formatted-body-impl-Bg-gx7rG.js → formatted-body-impl-0Lf8e1Ic.js} +2 -2
  28. package/public/assets/{formatted-body-impl-Bg-gx7rG.js.map → formatted-body-impl-0Lf8e1Ic.js.map} +1 -1
  29. package/public/assets/{index-BeDncjN4.js → index-C3su8ezq.js} +6 -6
  30. package/public/assets/{index-BeDncjN4.js.map → index-C3su8ezq.js.map} +1 -1
  31. package/public/assets/{insights-c08aW-7n.js → insights-64uExH1E.js} +2 -2
  32. package/public/assets/{insights-c08aW-7n.js.map → insights-64uExH1E.js.map} +1 -1
  33. package/public/assets/{integrations-BwpgcqnF.js → integrations-HyC1EaIp.js} +2 -2
  34. package/public/assets/{integrations-BwpgcqnF.js.map → integrations-HyC1EaIp.js.map} +1 -1
  35. package/public/assets/{maintenance-CTKP0kWB.js → maintenance-EVBpKH07.js} +2 -2
  36. package/public/assets/{maintenance-CTKP0kWB.js.map → maintenance-EVBpKH07.js.map} +1 -1
  37. package/public/assets/{managed-agents-BofCxo8d.js → managed-agents-BLZPxGuw.js} +2 -2
  38. package/public/assets/{managed-agents-BofCxo8d.js.map → managed-agents-BLZPxGuw.js.map} +1 -1
  39. package/public/assets/{markdown-preview-impl-CQ-6BIb8.js → markdown-preview-impl-Bg6rCWYN.js} +2 -2
  40. package/public/assets/{markdown-preview-impl-CQ-6BIb8.js.map → markdown-preview-impl-Bg6rCWYN.js.map} +1 -1
  41. package/public/assets/{memory-DvfXjMoi.js → memory-Bjr8bgjj.js} +2 -2
  42. package/public/assets/{memory-DvfXjMoi.js.map → memory-Bjr8bgjj.js.map} +1 -1
  43. package/public/assets/{messages-C9VsPzZ_.js → messages-CRYw1F4z.js} +2 -2
  44. package/public/assets/{messages-C9VsPzZ_.js.map → messages-CRYw1F4z.js.map} +1 -1
  45. package/public/assets/{orchestrators-MffVueeE.js → orchestrators-D-co1vdp.js} +2 -2
  46. package/public/assets/{orchestrators-MffVueeE.js.map → orchestrators-D-co1vdp.js.map} +1 -1
  47. package/public/assets/{overview-BSVajefM.js → overview-Ddy5czri.js} +2 -2
  48. package/public/assets/{overview-BSVajefM.js.map → overview-Ddy5czri.js.map} +1 -1
  49. package/public/assets/{pairs-B5711-Cs.js → pairs-CFVM8uHL.js} +2 -2
  50. package/public/assets/{pairs-B5711-Cs.js.map → pairs-CFVM8uHL.js.map} +1 -1
  51. package/public/assets/{security-DkUwK9Vj.js → security-Df4_76bh.js} +2 -2
  52. package/public/assets/{security-DkUwK9Vj.js.map → security-Df4_76bh.js.map} +1 -1
  53. package/public/assets/{select-DrM2RRiT.js → select-Cq3BBnL9.js} +2 -2
  54. package/public/assets/{select-DrM2RRiT.js.map → select-Cq3BBnL9.js.map} +1 -1
  55. package/public/assets/{settings-DdKCb-vy.js → settings-DOLEYXZL.js} +2 -2
  56. package/public/assets/{settings-DdKCb-vy.js.map → settings-DOLEYXZL.js.map} +1 -1
  57. package/public/assets/{store-iTiheuTD.js → store-C1E8jmNE.js} +4 -4
  58. package/public/assets/store-C1E8jmNE.js.map +1 -0
  59. package/public/assets/{tasks-e1y1PFfO.js → tasks-BcsmPi9_.js} +2 -2
  60. package/public/assets/{tasks-e1y1PFfO.js.map → tasks-BcsmPi9_.js.map} +1 -1
  61. package/public/assets/{teams-BUcxoGC_.js → teams-BQrV3eKI.js} +2 -2
  62. package/public/assets/{teams-BUcxoGC_.js.map → teams-BQrV3eKI.js.map} +1 -1
  63. package/public/assets/{terminal-viewer-impl-CmZjQOB2.js → terminal-viewer-impl-Dle04qpH.js} +2 -2
  64. package/public/assets/{terminal-viewer-impl-CmZjQOB2.js.map → terminal-viewer-impl-Dle04qpH.js.map} +1 -1
  65. package/public/assets/{work-queue-CXBQx43n.js → work-queue-BLhTLg4o.js} +2 -2
  66. package/public/assets/{work-queue-CXBQx43n.js.map → work-queue-BLhTLg4o.js.map} +1 -1
  67. package/public/assets/{workspaces-D2EfZh9a.js → workspaces-4NCNTmRQ.js} +3 -3
  68. package/public/assets/{workspaces-D2EfZh9a.js.map → workspaces-4NCNTmRQ.js.map} +1 -1
  69. package/public/index.html +3 -3
  70. package/runner/src/adapter.ts +2 -1
  71. package/runner/src/config.ts +143 -5
  72. package/src/agent-ref.ts +8 -6
  73. package/src/artifact-storage.ts +3 -5
  74. package/src/automations/constants.ts +12 -0
  75. package/src/automations/cron.ts +129 -0
  76. package/src/automations/crud.ts +84 -0
  77. package/src/automations/dispatch.ts +275 -0
  78. package/src/automations/index.ts +5 -0
  79. package/src/automations/reconcile.ts +245 -0
  80. package/src/automations/storage.ts +181 -0
  81. package/src/automations/types.ts +16 -0
  82. package/src/automations/validation.ts +140 -0
  83. package/src/automations.ts +1 -1045
  84. package/src/bus.ts +3 -4
  85. package/src/cli/_shared.ts +5 -4
  86. package/src/cli/agent-detect.ts +6 -4
  87. package/src/cli/introspect.ts +3 -2
  88. package/src/cli/token.ts +2 -1
  89. package/src/cli/workspace.ts +2 -1
  90. package/src/config.ts +176 -1
  91. package/src/connectors.ts +3 -3
  92. package/src/db/activity.ts +1 -1
  93. package/src/db/agent-predicates.ts +9 -0
  94. package/src/db/agent-search.ts +2 -2
  95. package/src/db/agents.ts +13 -41
  96. package/src/db/artifacts.ts +8 -8
  97. package/src/db/channel-identity.ts +38 -0
  98. package/src/db/channels.ts +37 -61
  99. package/src/db/connection.ts +2 -1
  100. package/src/db/continuation-archives.ts +7 -7
  101. package/src/db/delivery.ts +7 -12
  102. package/src/db/inbox.ts +1 -1
  103. package/src/db/integrations.ts +4 -4
  104. package/src/db/issue-lifecycle.ts +6 -6
  105. package/src/db/mappers.ts +1 -2
  106. package/src/db/merge-lease.ts +3 -3
  107. package/src/db/message-claims.ts +16 -0
  108. package/src/db/message-queries.ts +8 -0
  109. package/src/db/message-reads.ts +26 -9
  110. package/src/db/messages.ts +17 -41
  111. package/src/db/notification-subscriptions.ts +2 -2
  112. package/src/db/orchestrators.ts +4 -4
  113. package/src/db/pair-maintenance.ts +9 -0
  114. package/src/db/pairs.ts +10 -18
  115. package/src/db/plans.ts +1 -1
  116. package/src/db/policy-runtime.ts +14 -0
  117. package/src/db/project-entries.ts +1 -1
  118. package/src/db/projects.ts +2 -2
  119. package/src/db/shared.ts +7 -0
  120. package/src/db/stats.ts +2 -2
  121. package/src/db/task-core.ts +18 -0
  122. package/src/db/task-maintenance.ts +24 -0
  123. package/src/db/tasks.ts +5 -17
  124. package/src/db/teams.ts +3 -3
  125. package/src/db/workspaces.ts +6 -6
  126. package/src/index.ts +6 -5
  127. package/src/issue-tracker/github.ts +3 -3
  128. package/src/maintenance/constants.ts +52 -0
  129. package/src/maintenance/events.ts +32 -0
  130. package/src/maintenance/index.ts +7 -0
  131. package/src/maintenance/jobs.ts +340 -0
  132. package/src/maintenance/orphaned-sessions.ts +86 -0
  133. package/src/maintenance/scheduler.ts +231 -0
  134. package/src/maintenance/teams.ts +65 -0
  135. package/src/maintenance/types.ts +31 -0
  136. package/src/maintenance/workspaces.ts +251 -0
  137. package/src/maintenance.ts +1 -1099
  138. package/src/mcp/identity.ts +131 -0
  139. package/src/mcp/index.ts +208 -0
  140. package/src/mcp/tools-messaging.ts +459 -0
  141. package/src/mcp/tools-spawn.ts +145 -0
  142. package/src/mcp/tools-workspace.ts +227 -0
  143. package/src/mcp/types.ts +24 -0
  144. package/src/mcp/validation.ts +104 -0
  145. package/src/mcp.ts +1 -1319
  146. package/src/notification-router.ts +2 -2
  147. package/src/notification-rules.ts +2 -2
  148. package/src/project-ingest.ts +3 -3
  149. package/src/project-instructions.ts +1 -1
  150. package/src/provider-config-store.ts +5 -4
  151. package/src/provider-state.ts +2 -4
  152. package/src/reviewer-pipeline.ts +1 -1
  153. package/src/routes/agent-sessions.ts +5 -4
  154. package/src/routes/orchestrator-proxy.ts +5 -4
  155. package/src/routes/workspaces.ts +5 -4
  156. package/src/security.ts +13 -7
  157. package/src/self-resume.ts +3 -7
  158. package/src/services/agent-action.ts +2 -2
  159. package/src/services/issue-activity.ts +0 -4
  160. package/src/services/issue-associations.ts +2 -2
  161. package/src/services/issue-lifecycle.ts +8 -11
  162. package/src/services/managed-running.ts +1 -1
  163. package/src/services/parity-harness.ts +3 -3
  164. package/src/services/plan-graph.ts +4 -7
  165. package/src/services/send-message.ts +16 -12
  166. package/src/services/shutdown-agent.ts +2 -2
  167. package/src/services/team-templates.ts +2 -2
  168. package/src/services/team.ts +2 -2
  169. package/src/settings/modules.ts +1 -1
  170. package/src/settings/registry.ts +3 -3
  171. package/src/spawn-command.ts +1 -1
  172. package/src/sse.ts +0 -4
  173. package/src/upgrade.ts +7 -9
  174. package/src/workspace-actions.ts +1 -1
  175. package/src/workspace-auto-merge.ts +4 -5
  176. package/src/workspace-claim.ts +1 -2
  177. package/src/workspace-orphans.ts +2 -7
  178. package/src/workspace-probe.ts +5 -8
  179. package/public/assets/MessageBubble-DRrxfjyT.js.map +0 -1
  180. package/public/assets/display-DzNvOu1j.js.map +0 -1
  181. package/public/assets/store-iTiheuTD.js.map +0 -1
@@ -0,0 +1,131 @@
1
+ import { MAX_BODY_BYTES } from "../config";
2
+ import { compactAndResume } from "../self-resume";
3
+ import { getAgent, listAgents, searchContinuationArchives } from "../db";
4
+ import { resolveCallerAgentId } from "../agent-ref";
5
+ import { getComponentAuth, getIntegrationAuth, hasComponentScope, hasIntegrationScope } from "../security";
6
+ import { McpAuthError } from "../mcp-errors";
7
+ import { optionalEnum } from "../validation";
8
+ import type { McpAuthContext, ToolDefinition } from "./types";
9
+ import { optionalPositiveInt, optionalString, stringField } from "./validation";
10
+
11
+ export const IDENTITY_TOOLS: ToolDefinition[] = [
12
+ {
13
+ name: "relay_whoami",
14
+ description: "Return your own Relay identity and agent card (id, label, status, tags, capabilities). Your identity comes from your auth token — you do NOT need this to send, reply, or pair; use it only when you need to reason about yourself.",
15
+ requiredScopes: ["mcp:use"],
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {},
19
+ additionalProperties: false,
20
+ },
21
+ },
22
+ {
23
+ name: "relay_compact_and_resume",
24
+ description: "Self-resume this same agent in place: Relay stores the continuation envelope, compacts or clears context, then resumes with the relay-built continuation. Use at a clean seam after a context advisory.",
25
+ requiredScopes: ["command:write"],
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ workingState: { type: "string", description: "Current replaceable working state for the next generation." },
30
+ ruledOut: {
31
+ type: "array",
32
+ items: {
33
+ oneOf: [
34
+ { type: "string" },
35
+ {
36
+ type: "object",
37
+ properties: {
38
+ ruledOut: { type: "string" },
39
+ why: { type: "string" },
40
+ },
41
+ required: ["ruledOut"],
42
+ additionalProperties: false,
43
+ },
44
+ ],
45
+ },
46
+ },
47
+ mode: { type: "string", enum: ["clear-inject", "native"] },
48
+ objective: { type: "string", description: "Seed-only objective. Used only on the first call when no spawn prompt seeded the envelope." },
49
+ },
50
+ required: ["workingState"],
51
+ additionalProperties: false,
52
+ },
53
+ },
54
+ {
55
+ name: "relay_recall",
56
+ description: "Recall this same agent's archived pre-compaction transcript segments by keyword/BM25 search. Self-only: caller identity comes from the MCP token; no agentId parameter is accepted.",
57
+ requiredScopes: ["memory:read"],
58
+ inputSchema: {
59
+ type: "object",
60
+ properties: {
61
+ query: { type: "string", description: "Keyword query over archived compact/clear segments." },
62
+ limit: { type: "integer", minimum: 1, maximum: 20 },
63
+ },
64
+ required: ["query"],
65
+ additionalProperties: false,
66
+ },
67
+ },
68
+ ];
69
+
70
+ export function mcpAuthContext(req: Request): McpAuthContext {
71
+ const component = getComponentAuth(req);
72
+ if (component) return { actor: component.sub, kind: "component", scopes: component.scope, component };
73
+ const integration = getIntegrationAuth(req);
74
+ if (integration) return { actor: integration.name, kind: "integration", scopes: integration.scopes, integration };
75
+ return { actor: "server", kind: "server", scopes: ["*"] };
76
+ }
77
+
78
+ // THE caller-identity resolver for MCP. It delegates to src/agent-ref.ts so every
79
+ // transport resolves token constraints -> caller agent id through the same logic.
80
+ export function callerAgentId(auth: McpAuthContext): string | undefined {
81
+ if (auth.kind !== "component") return undefined;
82
+ return resolveCallerAgentId(auth.component?.constraints, listAgents);
83
+ }
84
+
85
+ export function relayWhoami(auth: McpAuthContext): Record<string, unknown> {
86
+ const agentId = callerAgentId(auth);
87
+ const agent = agentId ? getAgent(agentId) : null;
88
+ return {
89
+ agentId: agentId ?? null,
90
+ actor: auth.actor,
91
+ kind: auth.kind,
92
+ scopes: auth.scopes,
93
+ agent: agent ?? null,
94
+ };
95
+ }
96
+
97
+ export function relayCompactAndResume(auth: McpAuthContext, args: Record<string, unknown>): Record<string, unknown> {
98
+ const agentId = callerAgentId(auth);
99
+ if (!agentId) throw new McpAuthError("relay_compact_and_resume requires a single caller agent identity");
100
+ return compactAndResume({
101
+ agentId,
102
+ workingState: stringField(args.workingState, "workingState", { required: true, maxBytes: MAX_BODY_BYTES }),
103
+ ruledOut: args.ruledOut,
104
+ mode: optionalEnum(args.mode, "mode", ["clear-inject", "native"] as const),
105
+ objective: optionalString(args.objective, "objective", 64_000),
106
+ }) as unknown as Record<string, unknown>;
107
+ }
108
+
109
+ export function relayRecall(auth: McpAuthContext, args: Record<string, unknown>): Record<string, unknown> {
110
+ const agentId = callerAgentId(auth);
111
+ if (!agentId) throw new McpAuthError("relay_recall requires a single caller agent identity");
112
+ const query = stringField(args.query, "query", { required: true, max: 1000 });
113
+ const matches = searchContinuationArchives({
114
+ agentId,
115
+ query,
116
+ limit: optionalPositiveInt(args.limit, "limit"),
117
+ });
118
+ return { query, matches, count: matches.length };
119
+ }
120
+
121
+ export function hasAnyScope(auth: McpAuthContext, required: string[]): boolean {
122
+ if (auth.scopes.includes("*") || auth.scopes.includes("admin:*")) return true;
123
+ return required.some((scope) => hasScope(auth, scope));
124
+ }
125
+
126
+ export function hasScope(auth: McpAuthContext, scope: string): boolean {
127
+ if (auth.kind === "component" && auth.component) return hasComponentScope(auth.component, scope);
128
+ if (auth.kind === "integration" && auth.integration) return hasIntegrationScope(auth.integration, scope);
129
+ const [category] = scope.split(":");
130
+ return auth.scopes.includes(scope) || Boolean(category && auth.scopes.includes(`${category}:*`));
131
+ }
@@ -0,0 +1,208 @@
1
+ import { maxArtifactBytes } from "../artifact-storage";
2
+ import { readBodyBytes } from "../http-body";
3
+ import { MAX_BODY_BYTES, VERSION } from "../config";
4
+ import { spawnCapablePrimer } from "../spawn-targets";
5
+ import { McpAuthError, McpNotFoundError } from "../mcp-errors";
6
+ import { createActivityEvent, ValidationError } from "../db";
7
+ import { PLAN_TOOLS, relayPlanTool } from "../mcp-plan-tools";
8
+ import { TEAM_TOOLS, relayTeamTool } from "../mcp-team-tools";
9
+ import { BLACKBOARD_TOOLS, relayBlackboardTool } from "../mcp-blackboard-tools";
10
+ import { PROJECT_TOOLS, relayProjectTool } from "../mcp-project-tools"; import { ISSUE_REF_TOOLS, relayIssueRefTool } from "../mcp-issue-ref-tools"; import { ISSUE_ASSOCIATION_TOOLS, relayIssueAssociationTool } from "../mcp-issue-association-tools"; import { ISSUE_ACTIVITY_TOOLS, relayIssueActivityTool } from "../mcp-issue-activity-tools"; import { ISSUE_LIFECYCLE_TOOLS, relayIssueLifecycleTool } from "../mcp-issue-lifecycle-tools";
11
+ import { NOTIFICATION_SUBSCRIPTION_TOOLS, relayNotificationSubscriptionTool } from "../mcp-notification-subscription-tools"; import { MERGE_TOOLS, relayMergeTool } from "../mcp-merge-tools";
12
+ import { AGENT_ACTION_TOOL, relayAgentAction } from "../mcp-agent-action"; import { providerCatalogToolInputSchema } from "../mcp-provider-catalog";
13
+ import type { ActivityKind } from "../types";
14
+ import { worktreeMcpInstructions } from "../workspace-phase";
15
+ import { errMessage, isRecord } from "agent-relay-sdk";
16
+ import type { JsonRpcId, JsonRpcRequest, McpAuthContext, ToolDefinition } from "./types";
17
+ import { positiveIdOrUndefined, recordField, stringField } from "./validation";
18
+ import { IDENTITY_TOOLS, hasAnyScope, mcpAuthContext, relayCompactAndResume, relayRecall, relayWhoami } from "./identity";
19
+ import { MESSAGING_TOOLS, relayAgentStatus, relayAttachArtifact, relayFindAgents, relayGetMessage, relayGetThread, relayReply, relaySendMessage, relayUploadArtifact } from "./tools-messaging";
20
+ import { SPAWN_TOOLS, relayShutdownAgent, relaySpawnAgent, relaySpawnTargets } from "./tools-spawn";
21
+ import { WORKSPACE_TOOLS, callerIsolatedWorkspace, relayWorkspaceList, relayWorkspaceMutation, relayWorkspaceStatus } from "./tools-workspace";
22
+
23
+ const MCP_PROTOCOL_VERSION = "2024-11-05";
24
+ const MCP_BODY_OVERHEAD_BYTES = 16 * 1024;
25
+ const TOOLS: ToolDefinition[] = [
26
+ ...MESSAGING_TOOLS,
27
+ ...IDENTITY_TOOLS,
28
+ ...SPAWN_TOOLS,
29
+ AGENT_ACTION_TOOL,
30
+ ...WORKSPACE_TOOLS,
31
+ ...MERGE_TOOLS, ...TEAM_TOOLS,
32
+ ...PLAN_TOOLS, ...ISSUE_REF_TOOLS, ...ISSUE_ASSOCIATION_TOOLS, ...ISSUE_ACTIVITY_TOOLS, ...ISSUE_LIFECYCLE_TOOLS,
33
+ ...BLACKBOARD_TOOLS,
34
+ ...PROJECT_TOOLS,
35
+ ...NOTIFICATION_SUBSCRIPTION_TOOLS,
36
+ ];
37
+
38
+ export async function postMcp(req: Request): Promise<Response> {
39
+ const parsed = await parseJsonRpcRequest(req);
40
+ if (!parsed.ok) return Response.json(jsonRpcError(null, -32700, parsed.error), { status: parsed.status });
41
+ const request = parsed.request;
42
+ const id = request.id ?? null;
43
+ const method = request.method;
44
+ if (typeof method !== "string") return Response.json(jsonRpcError(id, -32600, "method required"));
45
+
46
+ try {
47
+ const auth = mcpAuthContext(req);
48
+ if (!hasAnyScope(auth, ["mcp:use"])) {
49
+ return Response.json(jsonRpcError(id, -32001, "MCP token missing mcp:use scope", { status: 403 }));
50
+ }
51
+
52
+ if (method === "initialize") {
53
+ // Mode-tailored primer (#214 §3): surface the worktree merge-back map only
54
+ // to callers that own an isolated worktree; shared-workspace agents omit it.
55
+ // #308: append the spawn primer only when the token actually grants command:spawn —
56
+ // authoritative scope-gating, eager + thin, so non-spawn agents get nothing.
57
+ const worktree = callerIsolatedWorkspace(auth);
58
+ const instructions = [
59
+ worktree ? worktreeMcpInstructions(worktree) : "",
60
+ spawnCapablePrimer({ canSpawn: hasAnyScope(auth, ["command:spawn"]), quota: auth.component?.constraints?.maxSpawnedAgents }),
61
+ ].filter(Boolean).join("\n\n");
62
+ return Response.json(jsonRpcResult(id, {
63
+ protocolVersion: MCP_PROTOCOL_VERSION,
64
+ capabilities: { tools: {} },
65
+ serverInfo: { name: "agent-relay", title: "Agent Relay", version: VERSION },
66
+ ...(instructions ? { instructions } : {}),
67
+ }));
68
+ }
69
+ if (method === "notifications/initialized") {
70
+ return new Response(null, { status: 202 });
71
+ }
72
+ if (method === "tools/list") {
73
+ return Response.json(jsonRpcResult(id, { tools: visibleTools(auth) }));
74
+ }
75
+ if (method === "tools/call") {
76
+ const result = await callTool(auth, request.params);
77
+ return Response.json(jsonRpcResult(id, result));
78
+ }
79
+ return Response.json(jsonRpcError(id, -32601, `unknown MCP method: ${method}`));
80
+ } catch (error) {
81
+ const message = errMessage(error);
82
+ const status = error instanceof McpAuthError ? 403 : error instanceof McpNotFoundError ? 404 : 400;
83
+ const code = error instanceof McpAuthError ? -32001 : error instanceof McpNotFoundError ? -32004 : -32602;
84
+ return Response.json(jsonRpcError(id, code, message, { status }));
85
+ }
86
+ }
87
+
88
+ async function parseJsonRpcRequest(req: Request): Promise<
89
+ | { ok: true; request: JsonRpcRequest }
90
+ | { ok: false; status: number; error: string }
91
+ > {
92
+ if (!req.body) return { ok: false, status: 400, error: "JSON-RPC body required" };
93
+ const maxBodyBytes = Math.max(MAX_BODY_BYTES, maxArtifactBytes() + MCP_BODY_OVERHEAD_BYTES);
94
+ const read = await readBodyBytes(req.body, maxBodyBytes);
95
+ if (!read.ok) return read;
96
+ try {
97
+ const body = JSON.parse(new TextDecoder().decode(read.bytes)) as unknown;
98
+ if (!isRecord(body)) return { ok: false, status: 400, error: "JSON-RPC body must be an object" };
99
+ return { ok: true, request: body as JsonRpcRequest };
100
+ } catch {
101
+ return { ok: false, status: 400, error: "invalid JSON-RPC body" };
102
+ }
103
+ }
104
+
105
+ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<string, unknown>> {
106
+ if (!isRecord(params)) throw new ValidationError("tools/call params must be an object");
107
+ const name = stringField(params.name, "name", { required: true });
108
+ const args = recordField(params.arguments ?? {}, "arguments");
109
+ const tool = TOOLS.find((item) => item.name === name);
110
+ if (!tool) throw new ValidationError(`unknown tool: ${name}`);
111
+ if (!hasAnyScope(auth, tool.requiredScopes)) {
112
+ auditToolCall(auth, name, "denied", undefined, `missing scope: ${tool.requiredScopes.join(" or ")}`);
113
+ throw new McpAuthError(`tool ${name} requires ${tool.requiredScopes.join(" or ")}`);
114
+ }
115
+
116
+ try {
117
+ let result: unknown;
118
+ if (name === "relay_send_message") result = relaySendMessage(auth, args);
119
+ else if (name === "relay_reply") result = relayReply(auth, args);
120
+ else if (name === "relay_get_message") result = relayGetMessage(args);
121
+ else if (name === "relay_get_thread") result = relayGetThread(args);
122
+ else if (name === "relay_upload_artifact") result = await relayUploadArtifact(auth, args);
123
+ else if (name === "relay_attach_artifact") result = relayAttachArtifact(auth, args);
124
+ else if (name === "relay_agent_status") result = relayAgentStatus(args);
125
+ else if (name === "relay_find_agents") result = relayFindAgents(auth, args);
126
+ else if (name === "relay_whoami") result = relayWhoami(auth);
127
+ else if (name === "relay_compact_and_resume") result = relayCompactAndResume(auth, args);
128
+ else if (name === "relay_recall") result = relayRecall(auth, args);
129
+ else if (name === "relay_spawn_agent") result = await relaySpawnAgent(auth, args);
130
+ else if (name === "relay_spawn_targets") result = relaySpawnTargets(auth);
131
+ else if (name === "relay_shutdown_agent") result = relayShutdownAgent(auth, args);
132
+ else if (name === "relay_agent_action") result = relayAgentAction(auth, args);
133
+ else if (name === "relay_workspace_status") result = await relayWorkspaceStatus(auth, args);
134
+ else if (name === "relay_workspace_list") result = relayWorkspaceList(auth, args);
135
+ else if (name === "relay_workspace_ready") result = relayWorkspaceMutation(auth, "ready", args);
136
+ else if (name === "relay_workspace_deps") result = relayWorkspaceMutation(auth, "deps-refresh", args);
137
+ else if (name === "relay_workspace_claim") result = relayWorkspaceMutation(auth, "claim", args);
138
+ else if (name === "relay_workspace_release") result = relayWorkspaceMutation(auth, "release-claim", args);
139
+ else if (name === "relay_workspace_land") result = relayWorkspaceMutation(auth, "merge", args);
140
+ else if (name.startsWith("relay_merge_")) result = relayMergeTool(auth, name, args); else if (name.startsWith("relay_team_")) result = await relayTeamTool(auth, name, args);
141
+ else if (name.startsWith("relay_plan_")) result = relayPlanTool(auth, name, args);
142
+ else if (name.startsWith("relay_blackboard_")) result = relayBlackboardTool(auth, name, args);
143
+ else if (name.startsWith("relay_project_")) result = relayProjectTool(auth, name, args); else if (name.startsWith("relay_issue_ref_")) result = relayIssueRefTool(auth, name, args); else if (name.startsWith("relay_issue_association_")) result = relayIssueAssociationTool(auth, name, args); else if (name.startsWith("relay_issue_activity_")) result = relayIssueActivityTool(auth, name, args); else if (name.startsWith("relay_issue_lifecycle_")) result = await relayIssueLifecycleTool(auth, name, args);
144
+ else if (name === "relay_subscribe" || name === "relay_unsubscribe" || name === "relay_list_subscriptions") result = relayNotificationSubscriptionTool(auth, name, args);
145
+ else throw new ValidationError(`unknown tool: ${name}`);
146
+
147
+ auditToolCall(auth, name, "ok", result);
148
+ return toolResult(result);
149
+ } catch (error) {
150
+ const message = errMessage(error);
151
+ auditToolCall(auth, name, error instanceof McpAuthError ? "denied" : "error", undefined, message);
152
+ throw error;
153
+ }
154
+ }
155
+
156
+ function visibleTools(auth: McpAuthContext): Array<Record<string, unknown>> {
157
+ return TOOLS
158
+ .filter((tool) => hasAnyScope(auth, tool.requiredScopes))
159
+ .map(({ name, description, alwaysLoad, ...tool }) => ({
160
+ name,
161
+ description,
162
+ inputSchema: providerCatalogToolInputSchema(name, tool.inputSchema),
163
+ ...(alwaysLoad ? { _meta: { "anthropic/alwaysLoad": true } } : {}),
164
+ }));
165
+ }
166
+
167
+ function toolResult(result: unknown): Record<string, unknown> {
168
+ return {
169
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
170
+ structuredContent: result,
171
+ };
172
+ }
173
+
174
+ function jsonRpcResult(id: JsonRpcId, result: unknown): Record<string, unknown> {
175
+ return { jsonrpc: "2.0", id, result };
176
+ }
177
+
178
+ function jsonRpcError(id: JsonRpcId, code: number, message: string, data?: unknown): Record<string, unknown> {
179
+ return { jsonrpc: "2.0", id, error: { code, message, ...(data ? { data } : {}) } };
180
+ }
181
+
182
+ function auditToolCall(
183
+ auth: McpAuthContext,
184
+ tool: string,
185
+ status: "ok" | "denied" | "error",
186
+ result?: unknown,
187
+ error?: string,
188
+ ): void {
189
+ try {
190
+ const messageId = isRecord(result) && isRecord(result.message)
191
+ ? positiveIdOrUndefined(result.message.id)
192
+ : isRecord(result) ? positiveIdOrUndefined(result.id) : undefined;
193
+ const kind: ActivityKind = tool.includes("reply") ? "reply" : tool.includes("send") ? "message" : "state";
194
+ createActivityEvent({
195
+ kind,
196
+ title: `MCP ${tool}`,
197
+ body: error ?? status,
198
+ meta: auth.actor,
199
+ icon: status === "ok" ? "ti-plug-connected" : status === "denied" ? "ti-shield-x" : "ti-alert-triangle",
200
+ view: "messages",
201
+ messageId,
202
+ agentId: auth.actor,
203
+ metadata: { source: "mcp", tool, actor: auth.actor, authKind: auth.kind, status },
204
+ });
205
+ } catch {
206
+ // Audit writes must not block tool calls.
207
+ }
208
+ }