@claude-flow/cli 3.71.3 → 3.72.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 (83) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +17 -0
  2. package/.claude/helpers/helpers.manifest.json +6 -6
  3. package/.claude/helpers/hook-handler.cjs +23 -10
  4. package/.claude/helpers/intelligence.cjs +23 -1
  5. package/.claude/helpers/statusline.cjs +29 -14
  6. package/README.md +18 -3
  7. package/bin/cli.js +13 -18
  8. package/bin/mcp-server.js +13 -18
  9. package/catalog-manifest.json +2 -2
  10. package/dist/src/appliance/rvfa-distribution.js +2 -2
  11. package/dist/src/benchmarks/gaia-critic.d.ts +1 -7
  12. package/dist/src/benchmarks/gaia-critic.js +1 -7
  13. package/dist/src/benchmarks/gaia-decomposer.d.ts +1 -8
  14. package/dist/src/benchmarks/gaia-decomposer.js +1 -8
  15. package/dist/src/commands/agent-wasm.js +2 -2
  16. package/dist/src/commands/analyze.js +1 -1
  17. package/dist/src/commands/daemon.js +1 -1
  18. package/dist/src/commands/doctor.js +3 -2
  19. package/dist/src/commands/guidance.js +9 -9
  20. package/dist/src/commands/hooks.js +13 -8
  21. package/dist/src/commands/init.js +14 -16
  22. package/dist/src/commands/memory.js +4 -3
  23. package/dist/src/commands/neural.js +8 -6
  24. package/dist/src/commands/route.js +1 -1
  25. package/dist/src/commands/ruvector/backup.js +7 -5
  26. package/dist/src/commands/ruvector/benchmark.js +6 -1
  27. package/dist/src/commands/ruvector/import.js +19 -5
  28. package/dist/src/commands/ruvector/init.js +1 -1
  29. package/dist/src/commands/start.js +1 -1
  30. package/dist/src/commands/status.js +1 -1
  31. package/dist/src/commands/task.js +18 -1
  32. package/dist/src/commands/update.js +1 -1
  33. package/dist/src/funnel/local-signals.js +7 -2
  34. package/dist/src/init/helpers-generator.js +16 -1
  35. package/dist/src/mcp-protocol.d.ts +90 -0
  36. package/dist/src/mcp-protocol.js +179 -0
  37. package/dist/src/mcp-server.d.ts +1 -0
  38. package/dist/src/mcp-server.js +20 -22
  39. package/dist/src/mcp-tools/agent-execute-core.js +9 -1
  40. package/dist/src/mcp-tools/agent-tools.d.ts +13 -0
  41. package/dist/src/mcp-tools/agent-tools.js +63 -12
  42. package/dist/src/mcp-tools/agentbbs-federation.d.ts +18 -1
  43. package/dist/src/mcp-tools/agentbbs-federation.js +23 -7
  44. package/dist/src/mcp-tools/browser-argv.d.ts +27 -0
  45. package/dist/src/mcp-tools/browser-argv.js +92 -0
  46. package/dist/src/mcp-tools/browser-session-tools.js +4 -0
  47. package/dist/src/mcp-tools/browser-tools.d.ts +1 -1
  48. package/dist/src/mcp-tools/browser-tools.js +25 -8
  49. package/dist/src/mcp-tools/coordination-tools.js +5 -4
  50. package/dist/src/mcp-tools/embeddings-tools.js +12 -4
  51. package/dist/src/mcp-tools/glob-exclusions.d.ts +6 -0
  52. package/dist/src/mcp-tools/glob-exclusions.js +81 -0
  53. package/dist/src/mcp-tools/hive-mind-tools.js +104 -35
  54. package/dist/src/mcp-tools/hooks-tools.js +12 -5
  55. package/dist/src/mcp-tools/import-limits.d.ts +4 -0
  56. package/dist/src/mcp-tools/import-limits.js +28 -0
  57. package/dist/src/mcp-tools/memory-tools.js +68 -72
  58. package/dist/src/mcp-tools/neural-tools.js +8 -2
  59. package/dist/src/mcp-tools/path-containment.d.ts +34 -0
  60. package/dist/src/mcp-tools/path-containment.js +133 -0
  61. package/dist/src/mcp-tools/performance-tools.js +7 -2
  62. package/dist/src/mcp-tools/record-store.d.ts +15 -0
  63. package/dist/src/mcp-tools/record-store.js +117 -3
  64. package/dist/src/mcp-tools/security-tools.d.ts +9 -0
  65. package/dist/src/mcp-tools/security-tools.js +39 -3
  66. package/dist/src/mcp-tools/session-tools.js +9 -41
  67. package/dist/src/mcp-tools/swarm-tools.js +36 -14
  68. package/dist/src/mcp-tools/task-tools.js +68 -5
  69. package/dist/src/mcp-tools/terminal-tools.js +16 -2
  70. package/dist/src/mcp-tools/validate-input.d.ts +2 -0
  71. package/dist/src/mcp-tools/validate-input.js +14 -0
  72. package/dist/src/mcp-tools/workflow-tools.js +13 -12
  73. package/dist/src/mcp-tools/x-federation-join.js +55 -14
  74. package/dist/src/memory/intelligence.d.ts +11 -0
  75. package/dist/src/memory/intelligence.js +84 -6
  76. package/dist/src/memory/memory-initializer.d.ts +6 -2
  77. package/dist/src/memory/memory-initializer.js +15 -6
  78. package/dist/src/ruvector/model-router.d.ts +6 -1
  79. package/dist/src/ruvector/model-router.js +16 -4
  80. package/dist/src/services/worker-daemon.js +30 -12
  81. package/dist/src/transfer/storage/gcs.js +3 -2
  82. package/package.json +1 -1
  83. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * MCP protocol-version handling for the stdio servers (ADR-524).
3
+ *
4
+ * Ruflo ships three stdio entry points (`bin/cli.js` auto-detect, `bin/mcp-server.js`,
5
+ * and `MCPServerManager` in mcp-server.ts). All three answered `initialize` with a
6
+ * hardcoded `2024-11-05` and knew nothing of the 2026-07-28 revision, which drops the
7
+ * handshake and carries the version on every request. This module is the one place
8
+ * that decides which revision a message speaks, so the three handlers cannot drift.
9
+ *
10
+ * Dual-era behaviour (spec: basic/versioning):
11
+ * - `initialize` selects legacy semantics; the requested legacy version is echoed.
12
+ * - a request carrying `_meta["io.modelcontextprotocol/protocolVersion"]` is served
13
+ * statelessly: `resultType` and `serverInfo` on every result, cache hints on lists,
14
+ * `-32602` for an unknown resource, `-32022` for an unsupported version.
15
+ * - `server/discover` is always answered, so a dual-era client's stdio probe succeeds.
16
+ */
17
+ export declare const MODERN_PROTOCOL_VERSIONS: readonly ["2026-07-28"];
18
+ export declare const LEGACY_PROTOCOL_VERSIONS: readonly ["2025-11-25", "2025-06-18", "2024-11-05"];
19
+ export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
20
+ /** Offered to a legacy client whose requested version we do not know. */
21
+ export declare const LATEST_LEGACY_PROTOCOL_VERSION: "2025-11-25";
22
+ export declare const META_PROTOCOL_VERSION = "io.modelcontextprotocol/protocolVersion";
23
+ export declare const META_CLIENT_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities";
24
+ export declare const META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
25
+ export declare const ERROR_INVALID_PARAMS = -32602;
26
+ export declare const ERROR_UNSUPPORTED_PROTOCOL_VERSION = -32022;
27
+ /** 2025-11-25 and earlier; replaced by -32602 in 2026-07-28. */
28
+ export declare const ERROR_LEGACY_RESOURCE_NOT_FOUND = -32002;
29
+ /** Tool catalogue is fixed for the life of a stdio process; five minutes is a conservative hint. */
30
+ export declare const TOOLS_LIST_TTL_MS = 300000;
31
+ export declare const DISCOVER_TTL_MS = 3600000;
32
+ export interface ServerIdentity {
33
+ name: string;
34
+ version: string;
35
+ }
36
+ export interface ProtocolContext {
37
+ /** True when the request used 2026-07-28 per-request metadata. */
38
+ modern: boolean;
39
+ /** Version the request declared, or undefined for legacy requests. */
40
+ version?: string;
41
+ /** Validated W3C traceparent, if the client propagated one. */
42
+ traceparent?: string;
43
+ tracestate?: string;
44
+ }
45
+ export interface JsonRpcMessage {
46
+ jsonrpc?: string;
47
+ id?: string | number | null;
48
+ method?: string;
49
+ params?: unknown;
50
+ }
51
+ export interface JsonRpcResponse {
52
+ jsonrpc: '2.0';
53
+ id?: string | number | null;
54
+ result?: Record<string, unknown>;
55
+ error?: {
56
+ code: number;
57
+ message: string;
58
+ data?: unknown;
59
+ };
60
+ }
61
+ export type Preflight = {
62
+ response: JsonRpcResponse;
63
+ context?: undefined;
64
+ } | {
65
+ response?: undefined;
66
+ context: ProtocolContext;
67
+ };
68
+ /** The CLI package version, read once from package.json (dist/src/ → ../../). */
69
+ export declare function rufloPackageVersion(): string;
70
+ export declare function defaultServerIdentity(): ServerIdentity;
71
+ /** Pick the legacy version to answer `initialize` with (spec: echo if supported, else our latest). */
72
+ export declare function negotiateLegacyVersion(requested: unknown): string;
73
+ export declare function isValidTraceparent(value: unknown): value is string;
74
+ /**
75
+ * Decide how to serve one inbound message before the handler's own switch runs.
76
+ * Returns a complete response for `initialize`, `server/discover` and protocol
77
+ * errors; otherwise returns the context the handler passes to `finalizeResponse`.
78
+ */
79
+ export declare function protocolPreflight(message: JsonRpcMessage, capabilities: Record<string, unknown>, identity?: ServerIdentity): Preflight;
80
+ /** The resource-not-found code the request's revision expects. */
81
+ export declare function resourceNotFoundCode(context: ProtocolContext): number;
82
+ /** Stable, name-ordered copy so `tools/list` is byte-identical across calls (prompt-cache friendly). */
83
+ export declare function sortToolsDeterministically<T extends {
84
+ name: string;
85
+ }>(tools: readonly T[]): T[];
86
+ /** Tool-call context fields to forward, so handlers and logs can join the caller's trace. */
87
+ export declare function traceContextFields(context: ProtocolContext): Record<string, string>;
88
+ /** Add the 2026-07-28 result fields to a handler's response. Legacy responses pass through untouched. */
89
+ export declare function finalizeResponse(response: JsonRpcResponse | null, method: string | undefined, context: ProtocolContext, identity?: ServerIdentity): JsonRpcResponse | null;
90
+ //# sourceMappingURL=mcp-protocol.d.ts.map
@@ -0,0 +1,179 @@
1
+ /**
2
+ * MCP protocol-version handling for the stdio servers (ADR-524).
3
+ *
4
+ * Ruflo ships three stdio entry points (`bin/cli.js` auto-detect, `bin/mcp-server.js`,
5
+ * and `MCPServerManager` in mcp-server.ts). All three answered `initialize` with a
6
+ * hardcoded `2024-11-05` and knew nothing of the 2026-07-28 revision, which drops the
7
+ * handshake and carries the version on every request. This module is the one place
8
+ * that decides which revision a message speaks, so the three handlers cannot drift.
9
+ *
10
+ * Dual-era behaviour (spec: basic/versioning):
11
+ * - `initialize` selects legacy semantics; the requested legacy version is echoed.
12
+ * - a request carrying `_meta["io.modelcontextprotocol/protocolVersion"]` is served
13
+ * statelessly: `resultType` and `serverInfo` on every result, cache hints on lists,
14
+ * `-32602` for an unknown resource, `-32022` for an unsupported version.
15
+ * - `server/discover` is always answered, so a dual-era client's stdio probe succeeds.
16
+ */
17
+ import { readFileSync } from 'node:fs';
18
+ export const MODERN_PROTOCOL_VERSIONS = ['2026-07-28'];
19
+ // 2025-03-26 is deliberately absent: it is the one revision that allowed JSON-RPC batching, which these
20
+ // line-at-a-time handlers do not accept. A client asking for it is offered 2025-11-25 instead.
21
+ export const LEGACY_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2024-11-05'];
22
+ export const SUPPORTED_PROTOCOL_VERSIONS = [
23
+ ...MODERN_PROTOCOL_VERSIONS,
24
+ ...LEGACY_PROTOCOL_VERSIONS,
25
+ ];
26
+ /** Offered to a legacy client whose requested version we do not know. */
27
+ export const LATEST_LEGACY_PROTOCOL_VERSION = LEGACY_PROTOCOL_VERSIONS[0];
28
+ export const META_PROTOCOL_VERSION = 'io.modelcontextprotocol/protocolVersion';
29
+ export const META_CLIENT_CAPABILITIES = 'io.modelcontextprotocol/clientCapabilities';
30
+ export const META_SERVER_INFO = 'io.modelcontextprotocol/serverInfo';
31
+ export const ERROR_INVALID_PARAMS = -32602;
32
+ export const ERROR_UNSUPPORTED_PROTOCOL_VERSION = -32022;
33
+ /** 2025-11-25 and earlier; replaced by -32602 in 2026-07-28. */
34
+ export const ERROR_LEGACY_RESOURCE_NOT_FOUND = -32002;
35
+ /** Tool catalogue is fixed for the life of a stdio process; five minutes is a conservative hint. */
36
+ export const TOOLS_LIST_TTL_MS = 300_000;
37
+ export const DISCOVER_TTL_MS = 3_600_000;
38
+ const LIST_METHODS = new Set(['tools/list', 'prompts/list', 'resources/list', 'resources/templates/list']);
39
+ // W3C Trace Context: version-traceid-parentid-flags, lowercase hex, ids not all zero, version != ff.
40
+ const TRACEPARENT = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
41
+ let cachedVersion;
42
+ /** The CLI package version, read once from package.json (dist/src/ → ../../). */
43
+ export function rufloPackageVersion() {
44
+ if (cachedVersion)
45
+ return cachedVersion;
46
+ for (const rel of ['../../package.json', '../package.json']) {
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(new URL(rel, import.meta.url), 'utf8'));
49
+ if (pkg.name === '@claude-flow/cli' && typeof pkg.version === 'string') {
50
+ cachedVersion = pkg.version;
51
+ return cachedVersion;
52
+ }
53
+ }
54
+ catch {
55
+ // try the next candidate
56
+ }
57
+ }
58
+ cachedVersion = '0.0.0';
59
+ return cachedVersion;
60
+ }
61
+ export function defaultServerIdentity() {
62
+ return { name: 'ruflo', version: rufloPackageVersion() };
63
+ }
64
+ /** Pick the legacy version to answer `initialize` with (spec: echo if supported, else our latest). */
65
+ export function negotiateLegacyVersion(requested) {
66
+ return typeof requested === 'string' && LEGACY_PROTOCOL_VERSIONS.includes(requested)
67
+ ? requested
68
+ : LATEST_LEGACY_PROTOCOL_VERSION;
69
+ }
70
+ export function isValidTraceparent(value) {
71
+ if (typeof value !== 'string')
72
+ return false;
73
+ const m = TRACEPARENT.exec(value);
74
+ if (!m)
75
+ return false;
76
+ const [, version, traceId, parentId] = m;
77
+ return version !== 'ff' && !/^0+$/.test(traceId) && !/^0+$/.test(parentId);
78
+ }
79
+ function metaOf(params) {
80
+ if (!params || typeof params !== 'object')
81
+ return undefined;
82
+ const meta = params._meta;
83
+ return meta && typeof meta === 'object' && !Array.isArray(meta) ? meta : undefined;
84
+ }
85
+ function error(id, code, message, data) {
86
+ return { jsonrpc: '2.0', id: id ?? null, error: data === undefined ? { code, message } : { code, message, data } };
87
+ }
88
+ /**
89
+ * Decide how to serve one inbound message before the handler's own switch runs.
90
+ * Returns a complete response for `initialize`, `server/discover` and protocol
91
+ * errors; otherwise returns the context the handler passes to `finalizeResponse`.
92
+ */
93
+ export function protocolPreflight(message, capabilities, identity = defaultServerIdentity()) {
94
+ const meta = metaOf(message.params);
95
+ const traceparent = isValidTraceparent(meta?.traceparent) ? meta.traceparent : undefined;
96
+ const tracestate = traceparent && typeof meta?.tracestate === 'string' && meta.tracestate.length <= 512
97
+ ? meta.tracestate
98
+ : undefined;
99
+ const declared = meta?.[META_PROTOCOL_VERSION];
100
+ const isNotification = message.id === undefined || message.id === null;
101
+ if (message.method === 'initialize') {
102
+ const requested = message.params?.protocolVersion;
103
+ return {
104
+ response: {
105
+ jsonrpc: '2.0',
106
+ id: message.id,
107
+ result: { protocolVersion: negotiateLegacyVersion(requested), serverInfo: identity, capabilities },
108
+ },
109
+ };
110
+ }
111
+ if (declared !== undefined && !isNotification) {
112
+ if (typeof declared !== 'string' || !SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
113
+ return {
114
+ response: error(message.id, ERROR_UNSUPPORTED_PROTOCOL_VERSION, 'Unsupported protocol version', {
115
+ supported: [...SUPPORTED_PROTOCOL_VERSIONS],
116
+ requested: declared,
117
+ }),
118
+ };
119
+ }
120
+ const modern = MODERN_PROTOCOL_VERSIONS.includes(declared);
121
+ if (modern) {
122
+ const caps = meta?.[META_CLIENT_CAPABILITIES];
123
+ if (message.method !== 'server/discover' && (!caps || typeof caps !== 'object' || Array.isArray(caps))) {
124
+ return { response: error(message.id, ERROR_INVALID_PARAMS, `Missing required _meta field: ${META_CLIENT_CAPABILITIES}`) };
125
+ }
126
+ }
127
+ if (message.method === 'server/discover')
128
+ return { response: discoverResponse(message.id, capabilities, identity) };
129
+ return { context: { modern, version: declared, traceparent, tracestate } };
130
+ }
131
+ if (message.method === 'server/discover')
132
+ return { response: discoverResponse(message.id, capabilities, identity) };
133
+ return { context: { modern: false, traceparent, tracestate } };
134
+ }
135
+ function discoverResponse(id, capabilities, identity) {
136
+ return {
137
+ jsonrpc: '2.0',
138
+ id,
139
+ result: {
140
+ resultType: 'complete',
141
+ supportedVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
142
+ capabilities,
143
+ _meta: { [META_SERVER_INFO]: identity },
144
+ ttlMs: DISCOVER_TTL_MS,
145
+ cacheScope: 'private',
146
+ },
147
+ };
148
+ }
149
+ /** The resource-not-found code the request's revision expects. */
150
+ export function resourceNotFoundCode(context) {
151
+ return context.modern ? ERROR_INVALID_PARAMS : ERROR_LEGACY_RESOURCE_NOT_FOUND;
152
+ }
153
+ /** Stable, name-ordered copy so `tools/list` is byte-identical across calls (prompt-cache friendly). */
154
+ export function sortToolsDeterministically(tools) {
155
+ return [...tools].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
156
+ }
157
+ /** Tool-call context fields to forward, so handlers and logs can join the caller's trace. */
158
+ export function traceContextFields(context) {
159
+ const out = {};
160
+ if (context.traceparent)
161
+ out.traceparent = context.traceparent;
162
+ if (context.tracestate)
163
+ out.tracestate = context.tracestate;
164
+ return out;
165
+ }
166
+ /** Add the 2026-07-28 result fields to a handler's response. Legacy responses pass through untouched. */
167
+ export function finalizeResponse(response, method, context, identity = defaultServerIdentity()) {
168
+ if (!response || !context.modern || !response.result)
169
+ return response;
170
+ const result = { resultType: 'complete', ...response.result };
171
+ const priorMeta = result._meta && typeof result._meta === 'object' ? result._meta : {};
172
+ result._meta = { ...priorMeta, [META_SERVER_INFO]: identity };
173
+ if (method && LIST_METHODS.has(method)) {
174
+ result.ttlMs = TOOLS_LIST_TTL_MS;
175
+ result.cacheScope = 'private';
176
+ }
177
+ return { ...response, result };
178
+ }
179
+ //# sourceMappingURL=mcp-protocol.js.map
@@ -182,6 +182,7 @@ export declare class MCPServerManager extends EventEmitter {
182
182
  * Handle incoming MCP message
183
183
  */
184
184
  private handleMCPMessage;
185
+ private dispatchMCPMessage;
185
186
  /**
186
187
  * Start HTTP server in-process
187
188
  */
@@ -26,6 +26,7 @@ import * as os from 'os';
26
26
  import { fileURLToPath } from 'url';
27
27
  import { dirname } from 'path';
28
28
  import { trackRequest } from './mcp-tools/request-tracker.js';
29
+ import { defaultServerIdentity, finalizeResponse, protocolPreflight, resourceNotFoundCode, sortToolsDeterministically, traceContextFields, } from './mcp-protocol.js';
29
30
  import { isPolicyEnforcementEnabled, loadMcpPolicy, evaluateToolCall, } from './mcp-tools/policy-enforcer.js';
30
31
  /** A failed task can be valid data; only an explicit error denotes tool failure. */
31
32
  function hasToolError(result) {
@@ -275,6 +276,11 @@ export function assessMcpSchemaOverhead(tools, contextWindowTokens) {
275
276
  : 'normal',
276
277
  };
277
278
  }
279
+ /** Capabilities every ruflo stdio server advertises (initialize and server/discover). */
280
+ const MCP_STDIO_CAPABILITIES = {
281
+ tools: { listChanged: true },
282
+ resources: {},
283
+ };
278
284
  /**
279
285
  * MCP Server Manager
280
286
  *
@@ -616,7 +622,7 @@ export class MCPServerManager extends EventEmitter {
616
622
  });
617
623
  // Import the tool registry
618
624
  const { listMCPTools, callMCPTool, hasTool } = await import('./mcp-client.js');
619
- const VERSION = '3.0.0';
625
+ const VERSION = defaultServerIdentity().version;
620
626
  const sessionId = `mcp-${Date.now()}-${randomUUID().slice(0, 8)}`;
621
627
  // Log to stderr to not corrupt stdout
622
628
  console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Starting in stdio mode`);
@@ -663,10 +669,7 @@ export class MCPServerManager extends EventEmitter {
663
669
  serverInfo: {
664
670
  name: 'ruflo',
665
671
  version: VERSION,
666
- capabilities: {
667
- tools: { listChanged: true },
668
- resources: {},
669
- },
672
+ capabilities: MCP_STDIO_CAPABILITIES,
670
673
  },
671
674
  },
672
675
  });
@@ -722,7 +725,6 @@ export class MCPServerManager extends EventEmitter {
722
725
  * Handle incoming MCP message
723
726
  */
724
727
  async handleMCPMessage(message, sessionId) {
725
- const { listMCPTools, callMCPTool, hasTool } = await import('./mcp-client.js');
726
728
  if (!message.method) {
727
729
  return {
728
730
  jsonrpc: '2.0',
@@ -730,24 +732,20 @@ export class MCPServerManager extends EventEmitter {
730
732
  error: { code: -32600, message: 'Invalid Request: missing method' },
731
733
  };
732
734
  }
735
+ // ADR-524: one decision point for which MCP revision this message speaks.
736
+ const preflight = protocolPreflight(message, MCP_STDIO_CAPABILITIES);
737
+ if (preflight.response)
738
+ return preflight.response;
739
+ const response = await this.dispatchMCPMessage(message, sessionId, preflight.context);
740
+ return finalizeResponse(response, message.method, preflight.context);
741
+ }
742
+ async dispatchMCPMessage(message, sessionId, protocol) {
743
+ const { listMCPTools, callMCPTool, hasTool } = await import('./mcp-client.js');
733
744
  const params = (message.params || {});
734
745
  try {
735
746
  switch (message.method) {
736
- case 'initialize':
737
- return {
738
- jsonrpc: '2.0',
739
- id: message.id,
740
- result: {
741
- protocolVersion: '2024-11-05',
742
- serverInfo: { name: 'ruflo', version: '3.0.0' },
743
- capabilities: {
744
- tools: { listChanged: true },
745
- resources: {},
746
- },
747
- },
748
- };
749
747
  case 'tools/list':
750
- const tools = filterAdvertisedMcpTools(listMCPTools(), this.options.tools);
748
+ const tools = sortToolsDeterministically(filterAdvertisedMcpTools(listMCPTools(), this.options.tools));
751
749
  return {
752
750
  jsonrpc: '2.0',
753
751
  id: message.id,
@@ -781,7 +779,7 @@ export class MCPServerManager extends EventEmitter {
781
779
  }
782
780
  }
783
781
  try {
784
- const result = await callMCPTool(toolName, toolParams, { sessionId, transport: 'stdio' });
782
+ const result = await callMCPTool(toolName, toolParams, { sessionId, transport: 'stdio', ...traceContextFields(protocol) });
785
783
  const isError = hasToolError(result);
786
784
  trackRequest(toolName, !isError);
787
785
  return {
@@ -808,7 +806,7 @@ export class MCPServerManager extends EventEmitter {
808
806
  case 'resources/templates/list':
809
807
  return { jsonrpc: '2.0', id: message.id, result: { resourceTemplates: [] } };
810
808
  case 'resources/read':
811
- return { jsonrpc: '2.0', id: message.id, error: { code: -32002, message: 'Resource not found' } };
809
+ return { jsonrpc: '2.0', id: message.id, error: { code: resourceNotFoundCode(protocol), message: 'Resource not found' } };
812
810
  case 'notifications/initialized':
813
811
  // Client notification - no response needed
814
812
  console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Client initialized`);
@@ -147,10 +147,18 @@ export async function callAnthropicMessages(input) {
147
147
  // checked in that order — matching callOllamaCompat's own precedence).
148
148
  const resolvedBaseUrl = process.env.OLLAMA_BASE_URL || persistedOllama?.baseUrl;
149
149
  const isSelfHosted = !!resolvedBaseUrl && !/^https:\/\/ollama\.com\/?$/i.test(resolvedBaseUrl);
150
+ // The persisted baseUrl comes from project config, which a cloned repo
151
+ // can commit. Never hand the environment's OLLAMA_API_KEY to an endpoint
152
+ // the project chose: that endpoint gets only the key saved beside it (or
153
+ // none). The env key goes to OLLAMA_BASE_URL, or to Ollama Cloud itself.
154
+ const baseFromProject = !process.env.OLLAMA_BASE_URL && !!persistedOllama?.baseUrl && isSelfHosted;
155
+ const apiKey = baseFromProject
156
+ ? persistedOllama?.apiKey || 'local'
157
+ : ollamaKey || persistedOllama?.apiKey || 'local';
150
158
  if (ollamaKey || persistedOllama?.apiKey || isSelfHosted) {
151
159
  return callOllamaCompat({
152
160
  ...input,
153
- apiKey: ollamaKey || persistedOllama?.apiKey || 'local',
161
+ apiKey,
154
162
  baseUrl: resolvedBaseUrl,
155
163
  model: input.model,
156
164
  // Local default for tier aliases / Anthropic ids (see
@@ -5,5 +5,18 @@
5
5
  * Includes model routing integration for intelligent model selection.
6
6
  */
7
7
  import { type MCPTool } from './types.js';
8
+ /** Live canonical registry members, excluding stale or duplicate swarm IDs. */
9
+ export declare function getLiveAgentIds(agentIds: string[]): string[];
10
+ export declare const AGENT_STATUSES: readonly ["idle", "busy", "terminated"];
11
+ type AgentLifecycleStatus = (typeof AGENT_STATUSES)[number];
12
+ /** Canonical lifecycle status (trimmed, any case, known aliases), or null. */
13
+ export declare function agentStatusOf(value: unknown): AgentLifecycleStatus | null;
14
+ /**
15
+ * Statuses `agent_list` should keep. `null` means no status filter:
16
+ * omitted, empty, whitespace, or `all` (any case, including inside a comma list).
17
+ * The caller still hides terminated agents unless includeTerminated is set.
18
+ */
19
+ export declare function agentListStatusFilter(status: unknown): string[] | null;
8
20
  export declare const agentTools: MCPTool[];
21
+ export {};
9
22
  //# sourceMappingURL=agent-tools.d.ts.map
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
- import { readRecordStore } from './record-store.js';
9
+ import { nullPrototypeRecords, readRecordStore } from './record-store.js';
10
10
  import { getProjectCwd } from './types.js';
11
11
  import { validateIdentifier, validateText, validateAgentSpawn } from './validate-input.js';
12
12
  import { executeAgentTask } from './agent-execute-core.js';
@@ -36,6 +36,11 @@ function ensureAgentDir() {
36
36
  function loadAgentStore() {
37
37
  return readRecordStore(getAgentPath(), 'agents', () => ({ agents: {}, version: '3.0.0' }));
38
38
  }
39
+ /** Live canonical registry members, excluding stale or duplicate swarm IDs. */
40
+ export function getLiveAgentIds(agentIds) {
41
+ const { agents } = loadAgentStore();
42
+ return [...new Set(agentIds)].filter(id => agents[id] && agents[id].status !== 'terminated');
43
+ }
39
44
  function saveAgentStore(store) {
40
45
  ensureAgentDir();
41
46
  writeFileSync(getAgentPath(), JSON.stringify(store, null, 2), 'utf-8');
@@ -50,14 +55,14 @@ function loadHiveAgents() {
50
55
  if (existsSync(path)) {
51
56
  const data = JSON.parse(readFileSync(path, 'utf-8'));
52
57
  if (data && typeof data.agents === 'object' && data.agents) {
53
- return data.agents;
58
+ return nullPrototypeRecords(data.agents);
54
59
  }
55
60
  }
56
61
  }
57
62
  catch {
58
63
  // Ignore — hive store is optional/best-effort.
59
64
  }
60
- return {};
65
+ return nullPrototypeRecords(undefined);
61
66
  }
62
67
  /**
63
68
  * #1916: merged view of every tracked agent — the canonical agent store
@@ -65,7 +70,41 @@ function loadHiveAgents() {
65
70
  * wins (it carries model-routing + lastResult that the hive store omits).
66
71
  */
67
72
  function loadAllAgents() {
68
- return { ...loadHiveAgents(), ...loadAgentStore().agents };
73
+ return Object.assign(nullPrototypeRecords(undefined), loadHiveAgents(), loadAgentStore().agents);
74
+ }
75
+ export const AGENT_STATUSES = ['idle', 'busy', 'terminated'];
76
+ // Spellings callers write for the three stored states. v2 `agent/list` used
77
+ // "active" for a working agent; hive liveness folds running/working into busy.
78
+ const AGENT_STATUS_ALIASES = {
79
+ active: 'busy', running: 'busy', working: 'busy',
80
+ ready: 'idle', waiting: 'idle',
81
+ stopped: 'terminated', inactive: 'terminated', offline: 'terminated',
82
+ };
83
+ /** Canonical lifecycle status (trimmed, any case, known aliases), or null. */
84
+ export function agentStatusOf(value) {
85
+ if (typeof value !== 'string')
86
+ return null;
87
+ const key = value.trim().toLowerCase();
88
+ if (AGENT_STATUSES.includes(key))
89
+ return key;
90
+ return Object.hasOwn(AGENT_STATUS_ALIASES, key) ? AGENT_STATUS_ALIASES[key] : null;
91
+ }
92
+ /**
93
+ * Statuses `agent_list` should keep. `null` means no status filter:
94
+ * omitted, empty, whitespace, or `all` (any case, including inside a comma list).
95
+ * The caller still hides terminated agents unless includeTerminated is set.
96
+ */
97
+ export function agentListStatusFilter(status) {
98
+ if (typeof status !== 'string' || status.trim() === '')
99
+ return null;
100
+ const canonical = status.split(',').map(part => part.trim()).filter(Boolean).map(part => {
101
+ if (part.toLowerCase() === 'all')
102
+ return 'all';
103
+ return agentStatusOf(part) ?? part.toLowerCase();
104
+ });
105
+ if (canonical.length === 0 || canonical.includes('all'))
106
+ return null;
107
+ return canonical;
69
108
  }
70
109
  // Default model mappings for agent types (can be overridden)
71
110
  const AGENT_TYPE_MODEL_DEFAULTS = {
@@ -568,17 +607,24 @@ export const agentTools = [
568
607
  inputSchema: {
569
608
  type: 'object',
570
609
  properties: {
571
- status: { type: 'string', description: 'Filter by status' },
610
+ status: { type: 'string', description: 'Filter by status (idle, busy, terminated; case-insensitive, aliases such as active/running/stopped). "all" or empty applies no status filter. Comma-separated values match any of them.' },
572
611
  domain: { type: 'string', description: 'Filter by domain' },
573
612
  agentType: { type: 'string', description: 'Filter by agent type' },
574
613
  includeTerminated: { type: 'boolean', description: 'Include terminated agents' },
575
614
  },
576
615
  },
577
616
  handler: async (input) => {
578
- if (input.status) {
579
- const v = validateIdentifier(input.status, 'status');
580
- if (!v.valid)
581
- return { agents: [], total: 0, error: `Input validation failed: ${v.error}` };
617
+ // Validate each status token. Commas separate values (`active,idle`);
618
+ // the whole string is not one identifier.
619
+ if (typeof input.status === 'string' && input.status.trim() !== '') {
620
+ for (const part of input.status.split(',')) {
621
+ const token = part.trim();
622
+ if (!token || token.toLowerCase() === 'all')
623
+ continue;
624
+ const v = validateIdentifier(token, 'status');
625
+ if (!v.valid)
626
+ return { agents: [], total: 0, error: `Input validation failed: ${v.error}` };
627
+ }
582
628
  }
583
629
  if (input.domain) {
584
630
  const v = validateIdentifier(input.domain, 'domain');
@@ -591,9 +637,14 @@ export const agentTools = [
591
637
  return { agents: [], total: 0, error: `Input validation failed: ${v.error}` };
592
638
  }
593
639
  let agents = Object.values(loadAllAgents()); // #1916: includes hive-mind-spawned workers
594
- // Filter by status
595
- if (input.status) {
596
- agents = agents.filter(a => a.status === input.status);
640
+ // Filter by status. `all` and empty mean no status filter (#4732),
641
+ // matching task_list: comma-separated, case-insensitive, known aliases.
642
+ const statuses = agentListStatusFilter(input.status);
643
+ if (statuses) {
644
+ agents = agents.filter(a => {
645
+ const stored = agentStatusOf(a.status) ?? String(a.status).trim().toLowerCase();
646
+ return statuses.includes(stored);
647
+ });
597
648
  }
598
649
  else if (!input.includeTerminated) {
599
650
  agents = agents.filter(a => a.status !== 'terminated');
@@ -143,6 +143,20 @@ export interface MergeResult {
143
143
  skippedUnverified: number;
144
144
  skippedOversize: number;
145
145
  skippedHopLimit: number;
146
+ /** Envelopes whose `origin` names a node the signature does not belong to. */
147
+ skippedForeignOrigin: number;
148
+ /** Highest `seq` among envelopes merged or already held; the next sync starts after it. */
149
+ acceptedMaxSeq: number;
150
+ }
151
+ /**
152
+ * Who may have signed an envelope. `origin` is inside the signature, but a
153
+ * signature only proves which key signed, not which node the envelope names.
154
+ * An envelope is accepted only when its origin is the syncing peer itself or
155
+ * another pinned peer, and it verifies against that origin's pinned key.
156
+ */
157
+ export interface OriginKeys {
158
+ peerNodeId: string;
159
+ pinned: ReadonlyMap<string, string>;
146
160
  }
147
161
  /**
148
162
  * Union-merge verified envelopes from a peer into the local room log.
@@ -150,8 +164,11 @@ export interface MergeResult {
150
164
  * Idempotent: `envelopeId` is the merge key, so replaying the same batch is a
151
165
  * no-op. Anything that fails verification is dropped and counted rather than
152
166
  * quarantined — a receiver has no use for an envelope it cannot attribute.
167
+ *
168
+ * `hops` is not signed (relays change it), so it only limits propagation and
169
+ * is never trusted for anything else.
153
170
  */
154
- export declare function mergeEnvelopes(basePath: string, roomId: string, incoming: SignedEnvelope[], peerPublicKey: string): Promise<MergeResult>;
171
+ export declare function mergeEnvelopes(basePath: string, roomId: string, incoming: SignedEnvelope[], peerPublicKey: string, origins?: OriginKeys): Promise<MergeResult>;
155
172
  /** Pull one room from one peer and merge what verifies. */
156
173
  export declare function syncRoomFromPeer(basePath: string, peer: FederationPeer, roomId: string, fetchImpl?: typeof fetch): Promise<MergeResult & {
157
174
  peerNodeId: string;
@@ -315,15 +315,19 @@ export function validateRoomId(roomId) {
315
315
  * Idempotent: `envelopeId` is the merge key, so replaying the same batch is a
316
316
  * no-op. Anything that fails verification is dropped and counted rather than
317
317
  * quarantined — a receiver has no use for an envelope it cannot attribute.
318
+ *
319
+ * `hops` is not signed (relays change it), so it only limits propagation and
320
+ * is never trusted for anything else.
318
321
  */
319
- export async function mergeEnvelopes(basePath, roomId, incoming, peerPublicKey) {
322
+ export async function mergeEnvelopes(basePath, roomId, incoming, peerPublicKey, origins) {
320
323
  validateRoomId(roomId);
321
324
  ensureDir(basePath);
322
325
  const result = {
323
326
  merged: 0, skippedDuplicate: 0, skippedUnverified: 0, skippedOversize: 0, skippedHopLimit: 0,
327
+ skippedForeignOrigin: 0, acceptedMaxSeq: 0,
324
328
  };
325
329
  const existing = readEnvelopes(basePath, roomId);
326
- const seen = new Set(existing.map(e => e.envelopeId));
330
+ const seen = new Map(existing.map(e => [e.envelopeId, Number(e.seq) || 0]));
327
331
  const logPath = roomLogPath(basePath, roomId);
328
332
  const batch = incoming.slice(0, MAX_ENVELOPES_PER_SYNC);
329
333
  for (const env of batch) {
@@ -337,6 +341,7 @@ export async function mergeEnvelopes(basePath, roomId, incoming, peerPublicKey)
337
341
  }
338
342
  if (seen.has(env.envelopeId)) {
339
343
  result.skippedDuplicate++;
344
+ result.acceptedMaxSeq = Math.max(result.acceptedMaxSeq, seen.get(env.envelopeId) ?? 0);
340
345
  continue;
341
346
  }
342
347
  if (JSON.stringify(env).length > MAX_ENVELOPE_BYTES) {
@@ -347,12 +352,22 @@ export async function mergeEnvelopes(basePath, roomId, incoming, peerPublicKey)
347
352
  result.skippedHopLimit++;
348
353
  continue;
349
354
  }
350
- if (!(await verifyEnvelope(env, peerPublicKey))) {
355
+ let signerKey = peerPublicKey;
356
+ if (origins) {
357
+ const key = env.origin === origins.peerNodeId ? peerPublicKey : typeof env.origin === 'string' ? origins.pinned.get(env.origin) : undefined;
358
+ if (!key) {
359
+ result.skippedForeignOrigin++;
360
+ continue;
361
+ }
362
+ signerKey = key;
363
+ }
364
+ if (!(await verifyEnvelope(env, signerKey))) {
351
365
  result.skippedUnverified++;
352
366
  continue;
353
367
  }
354
368
  appendFileSync(logPath, JSON.stringify({ ...env, hops: (env.hops ?? 0) + 1 }) + '\n');
355
- seen.add(env.envelopeId);
369
+ seen.set(env.envelopeId, Number(env.seq) || 0);
370
+ result.acceptedMaxSeq = Math.max(result.acceptedMaxSeq, Number(env.seq) || 0);
356
371
  result.merged++;
357
372
  }
358
373
  return result;
@@ -378,14 +393,15 @@ export async function syncRoomFromPeer(basePath, peer, roomId, fetchImpl = fetch
378
393
  clearTimeout(timer);
379
394
  }
380
395
  const envelopes = Array.isArray(body?.envelopes) ? body.envelopes : [];
381
- const merged = await mergeEnvelopes(basePath, roomId, envelopes, peer.publicKey);
382
396
  const peers = readPeers(basePath);
397
+ const pinned = new Map(peers.filter(p => typeof p.publicKey === 'string').map(p => [p.nodeId, p.publicKey]));
398
+ const merged = await mergeEnvelopes(basePath, roomId, envelopes, peer.publicKey, { peerNodeId: peer.nodeId, pinned });
383
399
  const rec = peers.find(p => p.nodeId === peer.nodeId);
384
400
  if (rec) {
385
401
  rec.lastSyncedAt = new Date().toISOString();
386
402
  rec.lastSeq = rec.lastSeq ?? {};
387
- const maxSeq = envelopes.reduce((m, e) => Math.max(m, Number(e.seq) || 0), since);
388
- rec.lastSeq[roomId] = maxSeq;
403
+ // Advance only past envelopes that verified; an unverifiable batch must not skip the cursor forward.
404
+ rec.lastSeq[roomId] = Math.max(since, merged.acceptedMaxSeq);
389
405
  writePeers(basePath, peers);
390
406
  }
391
407
  return { ...merged, peerNodeId: peer.nodeId, roomId };