mcp-coordinator 0.6.1 → 0.7.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.
Files changed (66) hide show
  1. package/README.md +24 -0
  2. package/dist/src/agent-activity.d.ts +13 -9
  3. package/dist/src/agent-activity.js +45 -24
  4. package/dist/src/agent-registry.d.ts +7 -7
  5. package/dist/src/agent-registry.js +19 -18
  6. package/dist/src/announce-workflow.d.ts +1 -0
  7. package/dist/src/announce-workflow.js +13 -12
  8. package/dist/src/auth/providers/registry.d.ts +4 -0
  9. package/dist/src/auth/providers/registry.js +7 -0
  10. package/dist/src/auth/providers/types.d.ts +11 -0
  11. package/dist/src/auth/providers/types.js +1 -0
  12. package/dist/src/auth.d.ts +24 -5
  13. package/dist/src/auth.js +172 -23
  14. package/dist/src/conflict-detector.d.ts +1 -0
  15. package/dist/src/conflict-detector.js +4 -4
  16. package/dist/src/consultation.d.ts +28 -14
  17. package/dist/src/consultation.js +101 -68
  18. package/dist/src/context-provider.d.ts +2 -2
  19. package/dist/src/context-provider.js +3 -4
  20. package/dist/src/database.js +203 -4
  21. package/dist/src/dependency-map.d.ts +25 -4
  22. package/dist/src/dependency-map.js +49 -11
  23. package/dist/src/file-tracker.d.ts +5 -4
  24. package/dist/src/file-tracker.js +16 -14
  25. package/dist/src/git-cochange-builder.d.ts +11 -2
  26. package/dist/src/git-cochange-builder.js +15 -7
  27. package/dist/src/http/handle-health.d.ts +9 -5
  28. package/dist/src/http/handle-health.js +22 -8
  29. package/dist/src/http/handle-rest.d.ts +3 -0
  30. package/dist/src/http/handle-rest.js +56 -55
  31. package/dist/src/http/utils.d.ts +4 -0
  32. package/dist/src/http/utils.js +7 -1
  33. package/dist/src/impact-scorer.d.ts +3 -0
  34. package/dist/src/impact-scorer.js +65 -51
  35. package/dist/src/introspection.d.ts +13 -7
  36. package/dist/src/introspection.js +34 -11
  37. package/dist/src/metrics.js +2 -1
  38. package/dist/src/mqtt-bridge.d.ts +3 -2
  39. package/dist/src/mqtt-bridge.js +33 -23
  40. package/dist/src/mqtt-broker.d.ts +16 -7
  41. package/dist/src/mqtt-broker.js +57 -15
  42. package/dist/src/security/audit.d.ts +11 -0
  43. package/dist/src/security/audit.js +7 -0
  44. package/dist/src/security/encryption.d.ts +17 -0
  45. package/dist/src/security/encryption.js +5 -0
  46. package/dist/src/serve-http.js +136 -57
  47. package/dist/src/server-setup.d.ts +12 -2
  48. package/dist/src/server-setup.js +33 -15
  49. package/dist/src/sse-emitter.d.ts +7 -4
  50. package/dist/src/sse-emitter.js +27 -21
  51. package/dist/src/tools/agents-tools.d.ts +2 -1
  52. package/dist/src/tools/agents-tools.js +36 -12
  53. package/dist/src/tools/consultation-tools.d.ts +2 -1
  54. package/dist/src/tools/consultation-tools.js +102 -36
  55. package/dist/src/tools/dependencies-tools.d.ts +2 -1
  56. package/dist/src/tools/dependencies-tools.js +25 -7
  57. package/dist/src/tools/files-tools.d.ts +2 -1
  58. package/dist/src/tools/files-tools.js +25 -7
  59. package/dist/src/tools/mqtt-tools.d.ts +7 -1
  60. package/dist/src/tools/mqtt-tools.js +27 -4
  61. package/dist/src/tools/status-tools.d.ts +7 -1
  62. package/dist/src/tools/status-tools.js +26 -9
  63. package/dist/src/types.d.ts +2 -0
  64. package/dist/src/working-files-tracker.d.ts +21 -11
  65. package/dist/src/working-files-tracker.js +32 -21
  66. package/package.json +1 -1
@@ -3,44 +3,68 @@ import { z } from "zod";
3
3
  * S1: agent registry MCP tools (4 tools).
4
4
  * register_agent, list_agents, heartbeat, agent_activity.
5
5
  */
6
- export function registerAgentTools(server, services, mcpLog) {
6
+ export function registerAgentTools(server, services, mcpLog, getSessionClaims) {
7
7
  const { registry, activityTracker, sseEmitter, mqttBridge } = services;
8
8
  server.tool("register_agent", "Register agent as online with module list", {
9
9
  agent_id: z.string(),
10
10
  name: z.string(),
11
11
  modules: z.array(z.string()),
12
- }, async ({ agent_id, name, modules }) => {
12
+ }, async ({ agent_id, name, modules }, extra) => {
13
+ const sessionId = extra.sessionId;
14
+ if (!sessionId)
15
+ throw new Error("MCP tool requires a session");
16
+ const claims = getSessionClaims(sessionId);
17
+ if (!claims)
18
+ throw new Error("Session has no captured claims (auth bug)");
13
19
  mcpLog.info({ tool: "register_agent", agent_id, name, module_count: modules.length }, "Tool called");
14
- const agent = registry.register(agent_id, name, modules);
15
- sseEmitter.emit("agent_online", { agent_id, name, modules });
20
+ const agent = registry.register(claims.org, agent_id, name, modules);
21
+ sseEmitter.emit("agent_online", { agent_id, name, modules }, { org_id: claims.org });
16
22
  mqttBridge.registerAgent(agent_id, name);
17
23
  return { content: [{ type: "text", text: JSON.stringify(agent) }] };
18
24
  });
19
25
  server.tool("list_agents", "List registered agents", {
20
26
  online_only: z.boolean().optional(),
21
- }, async ({ online_only }) => {
22
- const agents = online_only ? registry.listOnline() : registry.listAll();
27
+ }, async ({ online_only }, extra) => {
28
+ const sessionId = extra.sessionId;
29
+ if (!sessionId)
30
+ throw new Error("MCP tool requires a session");
31
+ const claims = getSessionClaims(sessionId);
32
+ if (!claims)
33
+ throw new Error("Session has no captured claims (auth bug)");
34
+ const agents = online_only ? registry.listOnline(claims.org) : registry.listAll(claims.org);
23
35
  return { content: [{ type: "text", text: JSON.stringify(agents) }] };
24
36
  });
25
37
  server.tool("heartbeat", "Update agent activity status and last seen timestamp", {
26
38
  agent_id: z.string(),
27
39
  current_file: z.string().optional(),
28
40
  current_thread: z.string().optional(),
29
- }, async ({ agent_id, current_file, current_thread }) => {
30
- registry.heartbeat(agent_id);
41
+ }, async ({ agent_id, current_file, current_thread }, extra) => {
42
+ const sessionId = extra.sessionId;
43
+ if (!sessionId)
44
+ throw new Error("MCP tool requires a session");
45
+ const claims = getSessionClaims(sessionId);
46
+ if (!claims)
47
+ throw new Error("Session has no captured claims (auth bug)");
48
+ registry.heartbeat(claims.org, agent_id);
31
49
  activityTracker.heartbeat(agent_id, {
32
50
  currentFile: current_file || null,
33
51
  currentThread: current_thread || null,
34
52
  });
35
- const activity = activityTracker.getActivity(agent_id);
53
+ const activity = activityTracker.getActivity(claims.org, agent_id);
36
54
  sseEmitter.emit("agent_activity", {
37
55
  agent_id, activity_status: activity.activity_status,
38
56
  current_file: activity.current_file, current_thread: activity.current_thread,
39
- });
57
+ }, { org_id: claims.org });
40
58
  return { content: [{ type: "text", text: JSON.stringify(activity) }] };
41
59
  });
42
- server.tool("agent_activity", "Get activity status for all online agents", {}, async () => {
43
- const activities = activityTracker.listAll({ idleAfterMinutes: 5 });
60
+ server.tool("agent_activity", "Get activity status for all online agents", {}, async (_args, extra) => {
61
+ const sessionId = extra.sessionId;
62
+ if (!sessionId)
63
+ throw new Error("MCP tool requires a session");
64
+ const claims = getSessionClaims(sessionId);
65
+ if (!claims)
66
+ throw new Error("Session has no captured claims (auth bug)");
67
+ const activities = activityTracker.listAll(claims.org, { idleAfterMinutes: 5 });
44
68
  return { content: [{ type: "text", text: JSON.stringify(activities) }] };
45
69
  });
46
70
  }
@@ -1,6 +1,7 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { CoordinatorServices } from "../server-setup.js";
3
3
  import type { Logger } from "../logger.js";
4
+ import type { AuthClaims } from "../auth.js";
4
5
  /**
5
6
  * S1 fix (partial): consultation domain tools extracted from server-setup.ts.
6
7
  *
@@ -18,4 +19,4 @@ import type { Logger } from "../logger.js";
18
19
  * straightforward following this pattern but kept out of this PR to minimize
19
20
  * the diff for reviewers.
20
21
  */
21
- export declare function registerConsultationTools(server: McpServer, services: CoordinatorServices, mcpLog: Logger): void;
22
+ export declare function registerConsultationTools(server: McpServer, services: CoordinatorServices, mcpLog: Logger, getSessionClaims: (sessionId: string) => AuthClaims | null): void;
@@ -18,7 +18,7 @@ import { runCommonAnnounceFlow } from "../announce-workflow.js";
18
18
  * straightforward following this pattern but kept out of this PR to minimize
19
19
  * the diff for reviewers.
20
20
  */
21
- export function registerConsultationTools(server, services, mcpLog) {
21
+ export function registerConsultationTools(server, services, mcpLog, getSessionClaims) {
22
22
  const { registry, consultation, conflictDetector, contextProvider, sseEmitter, mqttBridge } = services;
23
23
  server.tool("announce_work", "Open a consultation thread before starting work", {
24
24
  agent_id: z.string(),
@@ -32,10 +32,16 @@ export function registerConsultationTools(server, services, mcpLog) {
32
32
  assigned_to: z.string().optional().describe("Directed-dispatch: only this agent_id will be allowed to claim the thread. Use for lead→worker handoffs in maitre/chaine/relais presets. Implies keep_open=true."),
33
33
  target_symbols: z.array(z.string().max(256)).max(200).optional()
34
34
  .describe("Qualified symbol names you intend to touch (e.g. 'UserService.getById'). Used by Layer 0.5 to annotate same-file overlaps."),
35
- }, async ({ agent_id, subject, plan, target_modules, target_files, depends_on_files, exports_affected, keep_open, assigned_to, target_symbols }) => {
35
+ }, async ({ agent_id, subject, plan, target_modules, target_files, depends_on_files, exports_affected, keep_open, assigned_to, target_symbols }, extra) => {
36
+ const sessionId = extra.sessionId;
37
+ if (!sessionId)
38
+ throw new Error("MCP tool requires a session");
39
+ const claims = getSessionClaims(sessionId);
40
+ if (!claims)
41
+ throw new Error("Session has no captured claims (auth bug)");
36
42
  mcpLog.info({ tool: "announce_work", agent_id, subject, target_modules, target_files, assigned_to }, "Tool called");
37
- const conflicts = conflictDetector.detect({ agent_id, target_modules, target_files });
38
- const thread = consultation.announceWork({
43
+ const conflicts = conflictDetector.detect({ org_id: claims.org, agent_id, target_modules, target_files });
44
+ const thread = consultation.announceWork(claims.org, {
39
45
  agent_id, subject, plan, target_modules, target_files, depends_on_files, exports_affected, keep_open, assigned_to,
40
46
  });
41
47
  if (conflicts.length > 0) {
@@ -43,7 +49,7 @@ export function registerConsultationTools(server, services, mcpLog) {
43
49
  .run(JSON.stringify(conflicts), thread.id);
44
50
  }
45
51
  const { updated, categorized, respondents, planQuality } = runCommonAnnounceFlow(services, thread.id, {
46
- agent_id, subject, plan, target_modules, target_files, depends_on_files, exports_affected, keep_open, target_symbols,
52
+ org_id: claims.org, agent_id, subject, plan, target_modules, target_files, depends_on_files, exports_affected, keep_open, target_symbols,
47
53
  });
48
54
  sseEmitter.emit("thread_opened", {
49
55
  thread_id: thread.id, initiator: agent_id, subject, target_modules, conflicts,
@@ -51,9 +57,9 @@ export function registerConsultationTools(server, services, mcpLog) {
51
57
  mode: planQuality.mode,
52
58
  plan: plan || null,
53
59
  plan_quality: planQuality,
54
- });
60
+ }, { org_id: claims.org });
55
61
  mqttBridge.publishConsultation(thread.id, agent_id, subject, target_modules);
56
- const contextForInitiator = respondents.map((rid) => contextProvider.getRelevantContext(rid, { thread_id: updated.id, subject, target_modules, target_files })).filter((ctx) => ctx.modules.length > 0);
62
+ const contextForInitiator = respondents.map((rid) => contextProvider.getRelevantContext(claims.org, rid, { thread_id: updated.id, subject, target_modules, target_files })).filter((ctx) => ctx.modules.length > 0);
57
63
  return {
58
64
  content: [{
59
65
  type: "text",
@@ -69,17 +75,23 @@ export function registerConsultationTools(server, services, mcpLog) {
69
75
  content: z.string(),
70
76
  context_snapshot: z.string().optional(),
71
77
  in_reply_to: z.string().optional(),
72
- }, async ({ thread_id, agent_id, agent_name, type, content, context_snapshot, in_reply_to }) => {
78
+ }, async ({ thread_id, agent_id, agent_name, type, content, context_snapshot, in_reply_to }, extra) => {
79
+ const sessionId = extra.sessionId;
80
+ if (!sessionId)
81
+ throw new Error("MCP tool requires a session");
82
+ const claims = getSessionClaims(sessionId);
83
+ if (!claims)
84
+ throw new Error("Session has no captured claims (auth bug)");
73
85
  mcpLog.info({ tool: "post_to_thread", thread_id, agent_id, type }, "Tool called");
74
- const msg = consultation.postToThread({
86
+ const msg = consultation.postToThread(claims.org, {
75
87
  thread_id, agent_id, agent_name, type, content, context_snapshot, in_reply_to,
76
88
  });
77
- const thread = consultation.getThread(thread_id);
89
+ const thread = consultation.getThread(claims.org, thread_id);
78
90
  sseEmitter.emit("message_posted", {
79
91
  thread_id, agent_id, agent_name: agent_name || agent_id,
80
92
  type, content, round: thread?.round || 1,
81
93
  token_estimate: msg.token_estimate || 0,
82
- });
94
+ }, { org_id: claims.org });
83
95
  mqttBridge.publishMessage(thread_id, agent_id, type, content);
84
96
  return { content: [{ type: "text", text: JSON.stringify(msg) }] };
85
97
  });
@@ -88,65 +100,107 @@ export function registerConsultationTools(server, services, mcpLog) {
88
100
  agent_id: z.string(),
89
101
  summary: z.string(),
90
102
  plan: z.string().optional(),
91
- }, async ({ thread_id, agent_id, summary }) => {
103
+ }, async ({ thread_id, agent_id, summary }, extra) => {
104
+ const sessionId = extra.sessionId;
105
+ if (!sessionId)
106
+ throw new Error("MCP tool requires a session");
107
+ const claims = getSessionClaims(sessionId);
108
+ if (!claims)
109
+ throw new Error("Session has no captured claims (auth bug)");
92
110
  mcpLog.info({ tool: "propose_resolution", thread_id, agent_id }, "Tool called");
93
- consultation.proposeResolution(thread_id, agent_id, summary);
94
- sseEmitter.emit("resolution_proposed", { thread_id, agent_id, summary });
111
+ consultation.proposeResolution(claims.org, thread_id, agent_id, summary);
112
+ sseEmitter.emit("resolution_proposed", { thread_id, agent_id, summary }, { org_id: claims.org });
95
113
  mqttBridge.publishResolution(thread_id, "resolving", summary);
96
- const thread = consultation.getThread(thread_id);
114
+ const thread = consultation.getThread(claims.org, thread_id);
97
115
  return { content: [{ type: "text", text: JSON.stringify(thread) }] };
98
116
  });
99
117
  server.tool("approve_resolution", "Approve the proposed resolution", {
100
118
  thread_id: z.string(),
101
119
  agent_id: z.string(),
102
- }, async ({ thread_id, agent_id }) => {
120
+ }, async ({ thread_id, agent_id }, extra) => {
121
+ const sessionId = extra.sessionId;
122
+ if (!sessionId)
123
+ throw new Error("MCP tool requires a session");
124
+ const claims = getSessionClaims(sessionId);
125
+ if (!claims)
126
+ throw new Error("Session has no captured claims (auth bug)");
103
127
  mcpLog.info({ tool: "approve_resolution", thread_id, agent_id }, "Tool called");
104
- const agentInfo = registry.get(agent_id);
105
- consultation.approveResolution(thread_id, agent_id, agentInfo?.name);
106
- const thread = consultation.getThread(thread_id);
128
+ const agentInfo = registry.get(claims.org, agent_id);
129
+ consultation.approveResolution(claims.org, thread_id, agent_id, agentInfo?.name ?? undefined);
130
+ const thread = consultation.getThread(claims.org, thread_id);
107
131
  return { content: [{ type: "text", text: JSON.stringify(thread) }] };
108
132
  });
109
133
  server.tool("contest_resolution", "Contest the proposed resolution", {
110
134
  thread_id: z.string(),
111
135
  agent_id: z.string(),
112
136
  reason: z.string(),
113
- }, async ({ thread_id, agent_id, reason }) => {
137
+ }, async ({ thread_id, agent_id, reason }, extra) => {
138
+ const sessionId = extra.sessionId;
139
+ if (!sessionId)
140
+ throw new Error("MCP tool requires a session");
141
+ const claims = getSessionClaims(sessionId);
142
+ if (!claims)
143
+ throw new Error("Session has no captured claims (auth bug)");
114
144
  mcpLog.info({ tool: "contest_resolution", thread_id, agent_id }, "Tool called");
115
- consultation.contestResolution(thread_id, agent_id, reason);
116
- const thread = consultation.getThread(thread_id);
145
+ consultation.contestResolution(claims.org, thread_id, agent_id, reason);
146
+ const thread = consultation.getThread(claims.org, thread_id);
117
147
  return { content: [{ type: "text", text: JSON.stringify(thread) }] };
118
148
  });
119
149
  server.tool("close_thread", "Close a consultation thread", {
120
150
  thread_id: z.string(),
121
151
  agent_id: z.string(),
122
152
  summary: z.string(),
123
- }, async ({ thread_id, agent_id, summary }) => {
153
+ }, async ({ thread_id, agent_id, summary }, extra) => {
154
+ const sessionId = extra.sessionId;
155
+ if (!sessionId)
156
+ throw new Error("MCP tool requires a session");
157
+ const claims = getSessionClaims(sessionId);
158
+ if (!claims)
159
+ throw new Error("Session has no captured claims (auth bug)");
124
160
  mcpLog.info({ tool: "close_thread", thread_id, agent_id }, "Tool called");
125
- consultation.closeThread(thread_id, agent_id, summary);
161
+ consultation.closeThread(claims.org, thread_id, agent_id, summary);
126
162
  return { content: [{ type: "text", text: "closed" }] };
127
163
  });
128
164
  server.tool("cancel_thread", "Cancel a consultation thread", {
129
165
  thread_id: z.string(),
130
166
  agent_id: z.string(),
131
167
  reason: z.string().optional(),
132
- }, async ({ thread_id, agent_id, reason }) => {
168
+ }, async ({ thread_id, agent_id, reason }, extra) => {
169
+ const sessionId = extra.sessionId;
170
+ if (!sessionId)
171
+ throw new Error("MCP tool requires a session");
172
+ const claims = getSessionClaims(sessionId);
173
+ if (!claims)
174
+ throw new Error("Session has no captured claims (auth bug)");
133
175
  mcpLog.info({ tool: "cancel_thread", thread_id, agent_id }, "Tool called");
134
- consultation.cancelThread(thread_id, agent_id, reason);
135
- sseEmitter.emit("thread_cancelled", { thread_id, reason });
176
+ consultation.cancelThread(claims.org, thread_id, agent_id, reason ?? undefined);
177
+ sseEmitter.emit("thread_cancelled", { thread_id, reason }, { org_id: claims.org });
136
178
  return { content: [{ type: "text", text: "cancelled" }] };
137
179
  });
138
180
  server.tool("get_thread", "Get a thread with all messages", {
139
181
  thread_id: z.string(),
140
- }, async ({ thread_id }) => {
141
- const result = consultation.getThreadWithMessages(thread_id);
182
+ }, async ({ thread_id }, extra) => {
183
+ const sessionId = extra.sessionId;
184
+ if (!sessionId)
185
+ throw new Error("MCP tool requires a session");
186
+ const claims = getSessionClaims(sessionId);
187
+ if (!claims)
188
+ throw new Error("Session has no captured claims (auth bug)");
189
+ const result = consultation.getThreadWithMessages(claims.org, thread_id);
142
190
  mcpLog.debug({ tool: "get_thread", thread_id, message_count: result?.messages.length }, "Tool called");
143
191
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
144
192
  });
145
193
  server.tool("get_thread_updates", "Get new messages since timestamp", {
146
194
  agent_id: z.string(),
147
195
  since: z.string().optional(),
148
- }, async ({ agent_id, since }) => {
149
- const updates = consultation.getThreadUpdates(agent_id, since);
196
+ }, async ({ agent_id, since }, extra) => {
197
+ const sessionId = extra.sessionId;
198
+ if (!sessionId)
199
+ throw new Error("MCP tool requires a session");
200
+ const claims = getSessionClaims(sessionId);
201
+ if (!claims)
202
+ throw new Error("Session has no captured claims (auth bug)");
203
+ const updates = consultation.getThreadUpdates(claims.org, agent_id, since ?? undefined);
150
204
  return { content: [{ type: "text", text: JSON.stringify(updates) }] };
151
205
  });
152
206
  server.tool("list_threads", "List consultation threads", {
@@ -154,8 +208,14 @@ export function registerConsultationTools(server, services, mcpLog) {
154
208
  agent_id: z.string().optional(),
155
209
  module: z.string().optional(),
156
210
  assigned_to_me: z.string().optional().describe("Filter to threads claimable by this agent_id: open pool (assigned_to NULL) OR directed to me. Use for worker agents receiving directed dispatches."),
157
- }, async ({ status, agent_id, module, assigned_to_me }) => {
158
- const threads = consultation.listThreads({ status, agent_id, module, assigned_to_me });
211
+ }, async ({ status, agent_id, module, assigned_to_me }, extra) => {
212
+ const sessionId = extra.sessionId;
213
+ if (!sessionId)
214
+ throw new Error("MCP tool requires a session");
215
+ const claims = getSessionClaims(sessionId);
216
+ if (!claims)
217
+ throw new Error("Session has no captured claims (auth bug)");
218
+ const threads = consultation.listThreads(claims.org, { status, agent_id, module, assigned_to_me });
159
219
  mcpLog.debug({ tool: "list_threads", status, agent_id, module, assigned_to_me, result_count: threads.length }, "Tool called");
160
220
  return { content: [{ type: "text", text: JSON.stringify(threads) }] };
161
221
  });
@@ -164,9 +224,15 @@ export function registerConsultationTools(server, services, mcpLog) {
164
224
  agent_id: z.string(),
165
225
  file_path: z.string().optional().describe("Repo-relative file path."),
166
226
  summary: z.string(),
167
- }, async ({ session_id, agent_id, file_path, summary }) => {
168
- const result = consultation.logActionSummary({ session_id, agent_id, file_path, summary });
169
- sseEmitter.emit("action_summary", { agent_id, file_path, summary });
227
+ }, async ({ session_id, agent_id, file_path, summary }, extra) => {
228
+ const sessionId = extra.sessionId;
229
+ if (!sessionId)
230
+ throw new Error("MCP tool requires a session");
231
+ const claims = getSessionClaims(sessionId);
232
+ if (!claims)
233
+ throw new Error("Session has no captured claims (auth bug)");
234
+ const result = consultation.logActionSummary(claims.org, { session_id, agent_id, file_path, summary });
235
+ sseEmitter.emit("action_summary", { agent_id, file_path, summary }, { org_id: claims.org });
170
236
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
171
237
  });
172
238
  }
@@ -1,8 +1,9 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { CoordinatorServices } from "../server-setup.js";
3
3
  import type { Logger } from "../logger.js";
4
+ import type { AuthClaims } from "../auth.js";
4
5
  /**
5
6
  * S1: dependency map MCP tools (3 tools).
6
7
  * set_dependency_map, get_blast_radius, get_module_info.
7
8
  */
8
- export declare function registerDependenciesTools(server: McpServer, services: CoordinatorServices, _mcpLog: Logger): void;
9
+ export declare function registerDependenciesTools(server: McpServer, services: CoordinatorServices, _mcpLog: Logger, getSessionClaims: (sessionId: string) => AuthClaims | null): void;
@@ -3,25 +3,43 @@ import { z } from "zod";
3
3
  * S1: dependency map MCP tools (3 tools).
4
4
  * set_dependency_map, get_blast_radius, get_module_info.
5
5
  */
6
- export function registerDependenciesTools(server, services, _mcpLog) {
6
+ export function registerDependenciesTools(server, services, _mcpLog, getSessionClaims) {
7
7
  const { depMap } = services;
8
8
  server.tool("set_dependency_map", "Load module dependency graph", {
9
9
  modules: z.string(), // JSON DependencyMap
10
- }, async ({ modules }) => {
10
+ }, async ({ modules }, extra) => {
11
+ const sessionId = extra.sessionId;
12
+ if (!sessionId)
13
+ throw new Error("MCP tool requires a session");
14
+ const claims = getSessionClaims(sessionId);
15
+ if (!claims)
16
+ throw new Error("Session has no captured claims (auth bug)");
11
17
  const map = JSON.parse(modules);
12
- depMap.setMap(map);
18
+ depMap.setMap(claims.org, map);
13
19
  return { content: [{ type: "text", text: "ok" }] };
14
20
  });
15
21
  server.tool("get_blast_radius", "Calculate impact of changes to a module", {
16
22
  module_id: z.string(),
17
- }, async ({ module_id }) => {
18
- const radius = depMap.getBlastRadius(module_id);
23
+ }, async ({ module_id }, extra) => {
24
+ const sessionId = extra.sessionId;
25
+ if (!sessionId)
26
+ throw new Error("MCP tool requires a session");
27
+ const claims = getSessionClaims(sessionId);
28
+ if (!claims)
29
+ throw new Error("Session has no captured claims (auth bug)");
30
+ const radius = depMap.getBlastRadius(claims.org, module_id);
19
31
  return { content: [{ type: "text", text: JSON.stringify(radius) }] };
20
32
  });
21
33
  server.tool("get_module_info", "Get module dependency info", {
22
34
  module_id: z.string(),
23
- }, async ({ module_id }) => {
24
- const info = depMap.getModuleInfo(module_id);
35
+ }, async ({ module_id }, extra) => {
36
+ const sessionId = extra.sessionId;
37
+ if (!sessionId)
38
+ throw new Error("MCP tool requires a session");
39
+ const claims = getSessionClaims(sessionId);
40
+ if (!claims)
41
+ throw new Error("Session has no captured claims (auth bug)");
42
+ const info = depMap.getModuleInfo(claims.org, module_id);
25
43
  return { content: [{ type: "text", text: JSON.stringify(info) }] };
26
44
  });
27
45
  }
@@ -1,8 +1,9 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { CoordinatorServices } from "../server-setup.js";
3
3
  import type { Logger } from "../logger.js";
4
+ import type { AuthClaims } from "../auth.js";
4
5
  /**
5
6
  * S1: file tracking MCP tools (3 tools).
6
7
  * hot_files, get_session_files, check_file_conflict.
7
8
  */
8
- export declare function registerFilesTools(server: McpServer, services: CoordinatorServices, _mcpLog: Logger): void;
9
+ export declare function registerFilesTools(server: McpServer, services: CoordinatorServices, _mcpLog: Logger, getSessionClaims: (sessionId: string) => AuthClaims | null): void;
@@ -3,26 +3,44 @@ import { z } from "zod";
3
3
  * S1: file tracking MCP tools (3 tools).
4
4
  * hot_files, get_session_files, check_file_conflict.
5
5
  */
6
- export function registerFilesTools(server, services, _mcpLog) {
6
+ export function registerFilesTools(server, services, _mcpLog, getSessionClaims) {
7
7
  const { fileTracker } = services;
8
8
  server.tool("hot_files", "List files modified by multiple agents", {
9
9
  since_minutes: z.number().optional(),
10
- }, async ({ since_minutes }) => {
11
- const files = fileTracker.getHotFiles(since_minutes || 30);
10
+ }, async ({ since_minutes }, extra) => {
11
+ const sessionId = extra.sessionId;
12
+ if (!sessionId)
13
+ throw new Error("MCP tool requires a session");
14
+ const claims = getSessionClaims(sessionId);
15
+ if (!claims)
16
+ throw new Error("Session has no captured claims (auth bug)");
17
+ const files = fileTracker.getHotFiles(claims.org, since_minutes || 30);
12
18
  return { content: [{ type: "text", text: JSON.stringify(files) }] };
13
19
  });
14
20
  server.tool("get_session_files", "Get files modified in a session", {
15
21
  session_id: z.string(),
16
- }, async ({ session_id }) => {
17
- const files = fileTracker.getBySession(session_id);
22
+ }, async ({ session_id }, extra) => {
23
+ const sessionId = extra.sessionId;
24
+ if (!sessionId)
25
+ throw new Error("MCP tool requires a session");
26
+ const claims = getSessionClaims(sessionId);
27
+ if (!claims)
28
+ throw new Error("Session has no captured claims (auth bug)");
29
+ const files = fileTracker.getBySession(claims.org, session_id);
18
30
  return { content: [{ type: "text", text: JSON.stringify(files) }] };
19
31
  });
20
32
  server.tool("check_file_conflict", "Check if another agent is editing a file", {
21
33
  file_path: z.string().describe("Repo-relative file path."),
22
34
  agent_id: z.string(),
23
35
  within_minutes: z.number().optional(),
24
- }, async ({ file_path, agent_id, within_minutes }) => {
25
- const result = fileTracker.checkFileConflict(file_path, agent_id, within_minutes || 30);
36
+ }, async ({ file_path, agent_id, within_minutes }, extra) => {
37
+ const sessionId = extra.sessionId;
38
+ if (!sessionId)
39
+ throw new Error("MCP tool requires a session");
40
+ const claims = getSessionClaims(sessionId);
41
+ if (!claims)
42
+ throw new Error("Session has no captured claims (auth bug)");
43
+ const result = fileTracker.checkFileConflict(claims.org, file_path, agent_id, within_minutes || 30);
26
44
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
27
45
  });
28
46
  }
@@ -1,9 +1,15 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { CoordinatorServices } from "../server-setup.js";
3
3
  import type { Logger } from "../logger.js";
4
+ import type { AuthClaims } from "../auth.js";
4
5
  /**
5
6
  * S1: MQTT listener MCP tools (3 tools).
6
7
  * wait_for_message, get_queued_messages, mqtt_publish.
7
8
  * Replaces what used to be a standalone mqtt-mcp-bridge sidecar.
9
+ *
10
+ * Note (Task 23.5): the MqttBridge message-queue API is keyed by agent_id,
11
+ * not by org. MQTT topic scoping and per-org ACLs are deferred to Task 22.
12
+ * We still require valid session claims here so callers are authenticated,
13
+ * even though the underlying bridge calls don't yet filter by claims.org.
8
14
  */
9
- export declare function registerMqttTools(server: McpServer, services: CoordinatorServices, _mcpLog: Logger): void;
15
+ export declare function registerMqttTools(server: McpServer, services: CoordinatorServices, _mcpLog: Logger, getSessionClaims: (sessionId: string) => AuthClaims | null): void;
@@ -3,13 +3,24 @@ import { z } from "zod";
3
3
  * S1: MQTT listener MCP tools (3 tools).
4
4
  * wait_for_message, get_queued_messages, mqtt_publish.
5
5
  * Replaces what used to be a standalone mqtt-mcp-bridge sidecar.
6
+ *
7
+ * Note (Task 23.5): the MqttBridge message-queue API is keyed by agent_id,
8
+ * not by org. MQTT topic scoping and per-org ACLs are deferred to Task 22.
9
+ * We still require valid session claims here so callers are authenticated,
10
+ * even though the underlying bridge calls don't yet filter by claims.org.
6
11
  */
7
- export function registerMqttTools(server, services, _mcpLog) {
12
+ export function registerMqttTools(server, services, _mcpLog, getSessionClaims) {
8
13
  const { mqttBridge } = services;
9
14
  server.tool("wait_for_message", "Block until an MQTT consultation message arrives or timeout", {
10
15
  agent_id: z.string(),
11
16
  timeout_seconds: z.number().optional(),
12
- }, async ({ agent_id, timeout_seconds }) => {
17
+ }, async ({ agent_id, timeout_seconds }, extra) => {
18
+ const sessionId = extra.sessionId;
19
+ if (!sessionId)
20
+ throw new Error("MCP tool requires a session");
21
+ const claims = getSessionClaims(sessionId);
22
+ if (!claims)
23
+ throw new Error("Session has no captured claims (auth bug)");
13
24
  const timeoutMs = (timeout_seconds || 15) * 1000;
14
25
  const msg = await mqttBridge.waitForMessage(agent_id, timeoutMs);
15
26
  if (msg) {
@@ -19,14 +30,26 @@ export function registerMqttTools(server, services, _mcpLog) {
19
30
  });
20
31
  server.tool("get_queued_messages", "Get all queued MQTT messages without blocking", {
21
32
  agent_id: z.string(),
22
- }, async ({ agent_id }) => {
33
+ }, async ({ agent_id }, extra) => {
34
+ const sessionId = extra.sessionId;
35
+ if (!sessionId)
36
+ throw new Error("MCP tool requires a session");
37
+ const claims = getSessionClaims(sessionId);
38
+ if (!claims)
39
+ throw new Error("Session has no captured claims (auth bug)");
23
40
  const messages = mqttBridge.getQueuedMessages(agent_id);
24
41
  return { content: [{ type: "text", text: JSON.stringify(messages) }] };
25
42
  });
26
43
  server.tool("mqtt_publish", "Publish a message to an MQTT topic", {
27
44
  topic: z.string(),
28
45
  payload: z.string(),
29
- }, async ({ topic, payload }) => {
46
+ }, async ({ topic, payload }, extra) => {
47
+ const sessionId = extra.sessionId;
48
+ if (!sessionId)
49
+ throw new Error("MCP tool requires a session");
50
+ const claims = getSessionClaims(sessionId);
51
+ if (!claims)
52
+ throw new Error("Session has no captured claims (auth bug)");
30
53
  mqttBridge.mqttPublish(topic, payload);
31
54
  return { content: [{ type: "text", text: "published" }] };
32
55
  });
@@ -1,8 +1,14 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { CoordinatorServices } from "../server-setup.js";
3
3
  import type { Logger } from "../logger.js";
4
+ import type { AuthClaims } from "../auth.js";
4
5
  /**
5
6
  * S1: status + coordination helper MCP tools (2 tools).
6
7
  * coordinator_status, wait_for_peers.
8
+ *
9
+ * Note (Task 23.5): both tools are org-scoped — coordinator_status reports
10
+ * agents/threads/files for the caller's org, and wait_for_peers polls the
11
+ * same org's registry. Neither surfaces system-global (cross-org) data, so
12
+ * both require valid claims and use claims.org throughout.
7
13
  */
8
- export declare function registerStatusTools(server: McpServer, services: CoordinatorServices, mcpLog: Logger): void;
14
+ export declare function registerStatusTools(server: McpServer, services: CoordinatorServices, mcpLog: Logger, getSessionClaims: (sessionId: string) => AuthClaims | null): void;
@@ -2,14 +2,25 @@ import { z } from "zod";
2
2
  /**
3
3
  * S1: status + coordination helper MCP tools (2 tools).
4
4
  * coordinator_status, wait_for_peers.
5
+ *
6
+ * Note (Task 23.5): both tools are org-scoped — coordinator_status reports
7
+ * agents/threads/files for the caller's org, and wait_for_peers polls the
8
+ * same org's registry. Neither surfaces system-global (cross-org) data, so
9
+ * both require valid claims and use claims.org throughout.
5
10
  */
6
- export function registerStatusTools(server, services, mcpLog) {
11
+ export function registerStatusTools(server, services, mcpLog, getSessionClaims) {
7
12
  const { registry, consultation, fileTracker, mqttBridge } = services;
8
- server.tool("coordinator_status", "Full system status", {}, async () => {
9
- const online = registry.listOnline();
10
- const openThreads = consultation.listThreads({ status: "open" });
11
- const resolvingThreads = consultation.listThreads({ status: "resolving" });
12
- const hotFiles = fileTracker.getHotFiles(30);
13
+ server.tool("coordinator_status", "Full system status", {}, async (_args, extra) => {
14
+ const sessionId = extra.sessionId;
15
+ if (!sessionId)
16
+ throw new Error("MCP tool requires a session");
17
+ const claims = getSessionClaims(sessionId);
18
+ if (!claims)
19
+ throw new Error("Session has no captured claims (auth bug)");
20
+ const online = registry.listOnline(claims.org);
21
+ const openThreads = consultation.listThreads(claims.org, { status: "open" });
22
+ const resolvingThreads = consultation.listThreads(claims.org, { status: "resolving" });
23
+ const hotFiles = fileTracker.getHotFiles(claims.org, 30);
13
24
  const status = {
14
25
  agents_online: online.length,
15
26
  agents: online.map((a) => ({ id: a.id, name: a.name, modules: JSON.parse(a.modules) })),
@@ -25,14 +36,20 @@ export function registerStatusTools(server, services, mcpLog) {
25
36
  agent_id: z.string(),
26
37
  min_peers: z.number().optional(),
27
38
  timeout_seconds: z.number().optional(),
28
- }, async ({ agent_id, min_peers, timeout_seconds }) => {
39
+ }, async ({ agent_id, min_peers, timeout_seconds }, extra) => {
40
+ const sessionId = extra.sessionId;
41
+ if (!sessionId)
42
+ throw new Error("MCP tool requires a session");
43
+ const claims = getSessionClaims(sessionId);
44
+ if (!claims)
45
+ throw new Error("Session has no captured claims (auth bug)");
29
46
  const targetPeers = min_peers ?? 1;
30
47
  const timeoutMs = (timeout_seconds ?? 30) * 1000;
31
48
  const pollIntervalMs = 1000;
32
49
  const startedAt = Date.now();
33
50
  mcpLog.info({ tool: "wait_for_peers", agent_id, min_peers: targetPeers, timeout_seconds: timeoutMs / 1000 }, "Tool called");
34
51
  while (Date.now() - startedAt < timeoutMs) {
35
- const peers = registry.listOnline().filter((a) => a.id !== agent_id);
52
+ const peers = registry.listOnline(claims.org).filter((a) => a.id !== agent_id);
36
53
  if (peers.length >= targetPeers) {
37
54
  return {
38
55
  content: [{
@@ -47,7 +64,7 @@ export function registerStatusTools(server, services, mcpLog) {
47
64
  }
48
65
  await new Promise((r) => setTimeout(r, pollIntervalMs));
49
66
  }
50
- const finalPeers = registry.listOnline().filter((a) => a.id !== agent_id);
67
+ const finalPeers = registry.listOnline(claims.org).filter((a) => a.id !== agent_id);
51
68
  return {
52
69
  content: [{
53
70
  type: "text",
@@ -2,6 +2,7 @@ export type AgentConnectionStatus = "online" | "offline";
2
2
  export type ActivityStatus = "working" | "idle" | "waiting" | "offline";
3
3
  export interface Agent {
4
4
  id: string;
5
+ org_id: string;
5
6
  name: string;
6
7
  modules: string;
7
8
  status: AgentConnectionStatus;
@@ -19,6 +20,7 @@ export type ThreadStatus = "open" | "resolving" | "resolved" | "cancelled" | "po
19
20
  export type ResolutionType = "consensus" | "auto_resolved" | "timeout" | "closed" | "max_rounds" | "agent_departure";
20
21
  export interface Thread {
21
22
  id: string;
23
+ org_id: string;
22
24
  initiator_id: string;
23
25
  subject: string;
24
26
  plan: string | null;