@evomap/evolver-mcp 2.0.0-beta.13 → 2.0.0-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/envFile.d.ts CHANGED
@@ -7,4 +7,5 @@ export interface EnvFileLoadResult {
7
7
  export declare function expandHomePath(path: string): string;
8
8
  export declare function parseEnvFile(raw: string): Record<string, string>;
9
9
  export declare function loadEnvFile(path: string, env?: Record<string, string | undefined>): EnvFileLoadResult;
10
- export declare function loadEnvFileFromEnv(env?: Record<string, string | undefined>): EnvFileLoadResult;
10
+ export declare function loadEnvFileFromEnv(env?: Record<string, string | undefined>): EnvFileLoadResult;
11
+ export declare function loadEnvFileFromEnvOrThrow(env?: Record<string, string | undefined>): EnvFileLoadResult;
package/dist/envFile.js CHANGED
@@ -48,6 +48,12 @@ export function loadEnvFileFromEnv(env = process.env) {
48
48
  return { loaded: false, keys: [] };
49
49
  return loadEnvFile(path, env);
50
50
  }
51
+ export function loadEnvFileFromEnvOrThrow(env = process.env) {
52
+ const result = loadEnvFileFromEnv(env);
53
+ if (result.error)
54
+ throw new Error('failed to load EVOLVER_ENV_FILE');
55
+ return result;
56
+ }
51
57
  function unquoteEnvValue(value) {
52
58
  if (value.length >= 2) {
53
59
  const first = value[0];
@@ -13,6 +13,7 @@ export interface ProxyFetch {
13
13
  export interface EvolverProxyClientOptions {
14
14
  baseUrl: string;
15
15
  token: string;
16
+ expectedHubMode?: 'public' | 'private';
16
17
  fetchFn?: ProxyFetch;
17
18
  reloadSettings?: () => EvolverProxyClientOptions | undefined;
18
19
  }
@@ -23,13 +24,16 @@ export interface ProxySearchArgs {
23
24
  category?: string;
24
25
  gene?: string;
25
26
  limit?: number;
27
+ expectedHubMode?: 'public' | 'private';
26
28
  }
27
29
  export interface ProxyFetchArgs {
28
30
  assetId?: string;
29
31
  assetIds?: string[];
32
+ expectedHubMode?: 'public' | 'private';
30
33
  }
31
34
  export interface ProxyAssetBundle {
32
35
  assets: unknown[];
36
+ expected_hub_mode?: 'public' | 'private';
33
37
  }
34
38
  export interface ProxyReuseResultArgs {
35
39
  assetId: string;
@@ -40,6 +44,7 @@ export interface ProxyReuseResultArgs {
40
44
  tokensSaved?: number;
41
45
  timeSavedSeconds?: number;
42
46
  reason?: string;
47
+ expectedHubMode?: 'public' | 'private';
43
48
  }
44
49
  export interface ProxyAgentSearchArgs {
45
50
  query?: string;
@@ -59,6 +64,7 @@ export declare class EvolverProxyClient {
59
64
  private baseUrl;
60
65
  private token;
61
66
  private readonly fetchFn;
67
+ private readonly expectedHubMode?;
62
68
  private readonly reloadSettings;
63
69
  constructor(opts: EvolverProxyClientOptions);
64
70
  status(opts?: {
@@ -70,15 +76,21 @@ export declare class EvolverProxyClient {
70
76
  getAgentProfile(agentId: string, timeoutMs?: number): Promise<unknown>;
71
77
  discoverAgentsForTask(args: ProxyAgentDiscoverArgs): Promise<unknown>;
72
78
  submitAsset(asset: unknown): Promise<unknown>;
79
+ submitAssetBundle(bundle: ProxyAssetBundle): Promise<unknown>;
73
80
  /** Pre-publish dry-run: the hub runs its quality + content-safety gate but stores nothing and charges no credits. */
74
81
  validateAsset(asset: unknown): Promise<unknown>;
75
82
  validateAssetBundle(bundle: ProxyAssetBundle): Promise<unknown>;
76
83
  distillConversation(input: unknown): Promise<unknown>;
77
84
  recordReuseResult(args: ProxyReuseResultArgs): Promise<unknown>;
85
+ private modeBoundBody;
78
86
  call(method: string, path: string, body?: unknown, opts?: {
79
87
  signal?: AbortSignal;
80
88
  }): Promise<unknown>;
89
+ private verifyExpectedHubMode;
90
+ private verifyReloadedHubMode;
91
+ private acceptResult;
81
92
  private callOnce;
93
+ private connectionSnapshot;
82
94
  private reloadFromSettings;
83
95
  private proxyError;
84
96
  }
@@ -5,10 +5,12 @@ export class EvolverProxyClient {
5
5
  baseUrl;
6
6
  token;
7
7
  fetchFn;
8
+ expectedHubMode;
8
9
  reloadSettings;
9
10
  constructor(opts) {
10
11
  this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
11
12
  this.token = opts.token;
13
+ this.expectedHubMode = opts.expectedHubMode;
12
14
  this.fetchFn = opts.fetchFn ?? globalFetch;
13
15
  this.reloadSettings = opts.reloadSettings;
14
16
  }
@@ -16,6 +18,7 @@ export class EvolverProxyClient {
16
18
  return this.call('GET', '/proxy/status', undefined, opts);
17
19
  }
18
20
  search(args) {
21
+ const expectedHubMode = args.expectedHubMode ?? this.expectedHubMode;
19
22
  return this.call('POST', '/asset/search', {
20
23
  ...(args.text ? { text: args.text } : {}),
21
24
  ...(args.signalsAny && args.signalsAny.length > 0 ? { signals: args.signalsAny } : {}),
@@ -23,12 +26,15 @@ export class EvolverProxyClient {
23
26
  ...(args.category ? { category: args.category } : {}),
24
27
  ...(args.gene ? { gene: args.gene } : {}),
25
28
  ...(args.limit !== undefined ? { limit: args.limit } : {}),
29
+ ...(expectedHubMode ? { expected_hub_mode: expectedHubMode } : {}),
26
30
  });
27
31
  }
28
32
  fetchAsset(args) {
33
+ const expectedHubMode = args.expectedHubMode ?? this.expectedHubMode;
29
34
  return this.call('POST', '/asset/fetch', {
30
35
  ...(args.assetId ? { asset_id: args.assetId } : {}),
31
36
  ...(args.assetIds ? { asset_ids: args.assetIds } : {}),
37
+ ...(expectedHubMode ? { expected_hub_mode: expectedHubMode } : {}),
32
38
  });
33
39
  }
34
40
  searchAgents(args) {
@@ -45,19 +51,23 @@ export class EvolverProxyClient {
45
51
  });
46
52
  }
47
53
  submitAsset(asset) {
48
- return this.call('POST', '/asset/submit', { assets: [asset] });
54
+ return this.submitAssetBundle({ assets: [asset] });
55
+ }
56
+ submitAssetBundle(bundle) {
57
+ return this.call('POST', '/asset/submit', this.modeBoundBody(bundle));
49
58
  }
50
59
  /** Pre-publish dry-run: the hub runs its quality + content-safety gate but stores nothing and charges no credits. */
51
60
  validateAsset(asset) {
52
61
  return this.validateAssetBundle({ assets: [asset] });
53
62
  }
54
63
  validateAssetBundle(bundle) {
55
- return this.call('POST', '/asset/validate', bundle);
64
+ return this.call('POST', '/asset/validate', this.modeBoundBody(bundle));
56
65
  }
57
66
  distillConversation(input) {
58
- return this.call('POST', '/conversation/distill', input);
67
+ return this.call('POST', '/conversation/distill', this.modeBoundBody(input));
59
68
  }
60
69
  recordReuseResult(args) {
70
+ const expectedHubMode = args.expectedHubMode ?? this.expectedHubMode;
61
71
  return this.call('POST', '/asset/reuse-result', {
62
72
  asset_id: args.assetId,
63
73
  outcome: args.outcome,
@@ -65,43 +75,84 @@ export class EvolverProxyClient {
65
75
  ...(args.traceId ? { trace_id: args.traceId } : {}),
66
76
  ...(args.timeSavedSeconds !== undefined ? { time_saved_seconds: args.timeSavedSeconds } : {}),
67
77
  ...(args.reason ? { reason: args.reason } : {}),
78
+ ...(expectedHubMode ? { expected_hub_mode: expectedHubMode } : {}),
68
79
  });
69
80
  }
81
+ modeBoundBody(input) {
82
+ if (!this.expectedHubMode || !input || typeof input !== 'object' || Array.isArray(input))
83
+ return input;
84
+ const body = input;
85
+ return { ...body, expected_hub_mode: body['expected_hub_mode'] ?? this.expectedHubMode };
86
+ }
70
87
  async call(method, path, body, opts = {}) {
88
+ let connection = this.connectionSnapshot();
71
89
  try {
72
- const result = await this.callOnce(method, path, body, opts);
90
+ if (path !== '/proxy/status' && this.expectedHubMode === 'private') {
91
+ await this.verifyExpectedHubMode(connection, opts);
92
+ }
93
+ const result = await this.callOnce(method, path, body, opts, connection);
73
94
  if (result.ok)
74
- return result.parsed;
95
+ return this.acceptResult(result, path);
75
96
  if (result.status === 401 && this.reloadFromSettings()) {
76
- const retry = await this.callOnce(method, path, body, opts);
97
+ connection = this.connectionSnapshot();
98
+ await this.verifyReloadedHubMode(path, connection, opts);
99
+ const retry = await this.callOnce(method, path, body, opts, connection);
77
100
  if (retry.ok)
78
- return retry.parsed;
101
+ return this.acceptResult(retry, path);
79
102
  throw this.proxyError(retry, path);
80
103
  }
81
104
  throw this.proxyError(result, path);
82
105
  }
83
106
  catch (err) {
84
107
  if (this.reloadFromSettings()) {
85
- const retry = await this.callOnce(method, path, body, opts);
108
+ connection = this.connectionSnapshot();
109
+ await this.verifyReloadedHubMode(path, connection, opts);
110
+ const retry = await this.callOnce(method, path, body, opts, connection);
86
111
  if (retry.ok)
87
- return retry.parsed;
112
+ return this.acceptResult(retry, path);
88
113
  throw this.proxyError(retry, path);
89
114
  }
90
115
  throw err;
91
116
  }
92
117
  }
93
- async callOnce(method, path, body, opts) {
94
- const res = await this.fetchFn(`${this.baseUrl}${path}`, {
118
+ async verifyExpectedHubMode(connection, opts) {
119
+ // A proxy can restart on the same loopback URL with the same operator-supplied token. Verify every private
120
+ // operation against the same immutable connection snapshot used for its payload. This prevents a concurrent
121
+ // settings reload from moving the payload to an endpoint that the status probe never verified.
122
+ const result = await this.callOnce('GET', '/proxy/status', undefined, opts, connection);
123
+ if (!result.ok)
124
+ throw this.proxyError(result, '/proxy/status');
125
+ this.acceptResult(result, '/proxy/status');
126
+ }
127
+ async verifyReloadedHubMode(path, connection, opts) {
128
+ if (path !== '/proxy/status' && this.expectedHubMode === 'private') {
129
+ await this.verifyExpectedHubMode(connection, opts);
130
+ }
131
+ }
132
+ acceptResult(result, path) {
133
+ if (path === '/proxy/status' && this.expectedHubMode === 'private') {
134
+ const status = recordValue(result.parsed);
135
+ if (status['hub_mode'] !== 'private')
136
+ throw new Error('proxy_hub_mode_mismatch');
137
+ }
138
+ return result.parsed;
139
+ }
140
+ async callOnce(method, path, body, opts, connection) {
141
+ const res = await this.fetchFn(`${connection.baseUrl}${path}`, {
95
142
  method,
96
143
  headers: {
97
- authorization: `Bearer ${this.token}`,
144
+ authorization: `Bearer ${connection.token}`,
98
145
  'content-type': 'application/json',
146
+ ...(this.expectedHubMode ? { 'x-evomap-expected-hub-mode': this.expectedHubMode } : {}),
99
147
  },
100
148
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
101
149
  ...(opts.signal ? { signal: opts.signal } : {}),
102
150
  });
103
151
  return { ok: res.ok, status: res.status, parsed: await res.json() };
104
152
  }
153
+ connectionSnapshot() {
154
+ return { baseUrl: this.baseUrl, token: this.token };
155
+ }
105
156
  reloadFromSettings() {
106
157
  const next = this.reloadSettings?.();
107
158
  if (!next)
@@ -133,38 +184,51 @@ function agentDirectoryBody(args) {
133
184
  };
134
185
  }
135
186
  export function proxyClientFromEnv(env = process.env) {
187
+ const expectedHubMode = expectedHubModeFromEnv(env);
188
+ if (!expectedHubMode)
189
+ return undefined;
136
190
  const token = env['EVOLVER_IPC_TOKEN']?.trim();
137
191
  if (!token)
138
- return proxyClientFromSettings(env, env === process.env);
139
- const explicitUrl = env['EVOLVER_PROXY_URL']?.trim();
140
- const port = env['EVOLVER_IPC_PORT']?.trim() || env['EVOMAP_PROXY_PORT']?.trim() || '19820';
141
- return new EvolverProxyClient({ baseUrl: explicitUrl || `http://127.0.0.1:${port}`, token });
192
+ return proxyClientFromSettings(env, env === process.env, undefined, expectedHubMode);
193
+ const baseUrl = proxyBaseUrlFromEnv(env);
194
+ return baseUrl ? new EvolverProxyClient({ baseUrl, token, expectedHubMode }) : undefined;
142
195
  }
143
196
  export async function reachableProxyClientFromEnv(env = process.env, opts = {}) {
197
+ const expectedHubMode = expectedHubModeFromEnv(env);
198
+ if (!expectedHubMode)
199
+ return undefined;
144
200
  const token = env['EVOLVER_IPC_TOKEN']?.trim();
145
201
  if (token) {
146
- const explicitUrl = env['EVOLVER_PROXY_URL']?.trim();
147
- const port = env['EVOLVER_IPC_PORT']?.trim() || env['EVOMAP_PROXY_PORT']?.trim() || '19820';
148
- return new EvolverProxyClient({ baseUrl: explicitUrl || `http://127.0.0.1:${port}`, token, ...(opts.fetchFn ? { fetchFn: opts.fetchFn } : {}) });
202
+ const baseUrl = proxyBaseUrlFromEnv(env);
203
+ return baseUrl ? new EvolverProxyClient({ baseUrl, token, expectedHubMode, ...(opts.fetchFn ? { fetchFn: opts.fetchFn } : {}) }) : undefined;
149
204
  }
150
- const client = proxyClientFromSettings(env, env === process.env, opts.fetchFn);
205
+ const client = proxyClientFromSettings(env, env === process.env, opts.fetchFn, expectedHubMode);
151
206
  if (!client)
152
207
  return undefined;
153
208
  return await proxyClientReachable(client, opts.timeoutMs ?? 250) ? client : undefined;
154
209
  }
155
- function proxyClientFromSettings(env, allowDefaultHome, fetchFn) {
210
+ function proxyClientFromSettings(env, allowDefaultHome, fetchFn, expectedHubMode = 'public') {
156
211
  const settings = readProxySettings(env, allowDefaultHome);
157
212
  return settings ? new EvolverProxyClient({
158
213
  ...settings,
214
+ expectedHubMode,
159
215
  ...(fetchFn ? { fetchFn } : {}),
160
216
  reloadSettings: () => readProxySettings(env, allowDefaultHome),
161
217
  }) : undefined;
162
218
  }
219
+ function expectedHubModeFromEnv(env) {
220
+ const value = env['EVOMAP_HUB_MODE']?.trim().toLowerCase() || 'public';
221
+ return value === 'public' || value === 'private' ? value : undefined;
222
+ }
163
223
  function readProxySettings(env, allowDefaultHome) {
224
+ const explicitPath = env['EVOLVER_PROXY_SETTINGS_FILE']?.trim();
225
+ const settingsDir = env['EVOLVER_SETTINGS_DIR']?.trim();
164
226
  const homeDir = env['HOME']?.trim() || (allowDefaultHome ? homedir() : '');
165
- if (!homeDir)
227
+ const settingsPath = explicitPath
228
+ || (settingsDir ? join(settingsDir, 'settings.json') : undefined)
229
+ || (homeDir ? join(homeDir, '.evolver', 'settings.json') : undefined);
230
+ if (!settingsPath)
166
231
  return undefined;
167
- const settingsPath = join(homeDir, '.evolver', 'settings.json');
168
232
  try {
169
233
  if (!lstatSync(settingsPath).isFile())
170
234
  return undefined;
@@ -185,6 +249,8 @@ function isLoopbackHttpUrl(raw) {
185
249
  const url = new URL(raw);
186
250
  if (url.protocol !== 'http:' && url.protocol !== 'https:')
187
251
  return false;
252
+ if (url.username || url.password || url.search || url.hash || (url.pathname && url.pathname !== '/'))
253
+ return false;
188
254
  const hostname = url.hostname.toLowerCase();
189
255
  return hostname === '127.0.0.1'
190
256
  || hostname === 'localhost'
@@ -195,6 +261,19 @@ function isLoopbackHttpUrl(raw) {
195
261
  return false;
196
262
  }
197
263
  }
264
+ function proxyBaseUrlFromEnv(env) {
265
+ const explicitUrl = env['EVOLVER_PROXY_URL']?.trim();
266
+ if (explicitUrl)
267
+ return isLoopbackHttpUrl(explicitUrl) ? explicitUrl : undefined;
268
+ const rawPort = env['EVOLVER_IPC_PORT']?.trim() || env['EVOMAP_PROXY_PORT']?.trim() || '19820';
269
+ if (!/^\d+$/.test(rawPort))
270
+ return undefined;
271
+ const port = Number(rawPort);
272
+ if (!Number.isInteger(port) || port < 0 || port > 65_535)
273
+ return undefined;
274
+ const baseUrl = `http://127.0.0.1:${port}`;
275
+ return isLoopbackHttpUrl(baseUrl) ? baseUrl : undefined;
276
+ }
198
277
  function recordValue(value) {
199
278
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
200
279
  }
@@ -205,7 +284,9 @@ async function proxyClientReachable(client, timeoutMs) {
205
284
  await client.status({ signal: controller.signal });
206
285
  return true;
207
286
  }
208
- catch {
287
+ catch (error) {
288
+ if (error instanceof Error && error.message === 'proxy_hub_mode_mismatch')
289
+ throw error;
209
290
  return false;
210
291
  }
211
292
  finally {
@@ -214,5 +295,5 @@ async function proxyClientReachable(client, timeoutMs) {
214
295
  }
215
296
  }
216
297
  async function globalFetch(url, init) {
217
- return fetch(url, init);
298
+ return fetch(url, { ...init, redirect: 'error' });
218
299
  }
package/dist/stdio.js CHANGED
@@ -8,10 +8,13 @@ import { buildEvolverTools } from './tools.js';
8
8
  import { buildEvolverPrimer } from './primer.js';
9
9
  import { EvolverMcpServer, UnknownToolError } from './server.js';
10
10
  import { reachableProxyClientFromEnv } from './proxyClient.js';
11
- import { loadEnvFileFromEnv } from './envFile.js';
12
- const envFile = loadEnvFileFromEnv(process.env);
13
- if (envFile.error) {
14
- process.stderr.write(`[evolver-mcp] failed to load EVOLVER_ENV_FILE: ${envFile.error}\n`);
11
+ import { loadEnvFileFromEnvOrThrow } from './envFile.js';
12
+ try {
13
+ loadEnvFileFromEnvOrThrow(process.env);
14
+ }
15
+ catch {
16
+ process.stderr.write('[evolver-mcp] fatal: failed to load EVOLVER_ENV_FILE\n');
17
+ process.exit(1);
15
18
  }
16
19
  const store = new assetstore.LocalJsonlProvider(events.assetsDir());
17
20
  const mailboxPath = process.env['EVOLVER_MCP_MAILBOX'] ?? join(events.evomapHome(), 'mailbox', 'mcp.db');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver-mcp",
3
- "version": "2.0.0-beta.13",
3
+ "version": "2.0.0-beta.15",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Evolver MCP server (agent 工具发现入口)",
@@ -20,7 +20,7 @@
20
20
  }
21
21
  },
22
22
  "dependencies": {
23
- "@evomap/evolver-core": "2.0.0-beta.13",
23
+ "@evomap/evolver-core": "2.0.0-beta.15",
24
24
  "smol-toml": "^1.6.1"
25
25
  },
26
26
  "repository": {