@evomap/evolver-mcp 2.0.0-beta.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.
- package/dist/codexInstaller.d.ts +34 -0
- package/dist/codexInstaller.js +171 -0
- package/dist/cursorRulesInstaller.d.ts +76 -0
- package/dist/cursorRulesInstaller.js +196 -0
- package/dist/envFile.d.ts +10 -0
- package/dist/envFile.js +68 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/injection.d.ts +56 -0
- package/dist/injection.js +84 -0
- package/dist/installer.d.ts +106 -0
- package/dist/installer.js +513 -0
- package/dist/manualWiring.d.ts +14 -0
- package/dist/manualWiring.js +91 -0
- package/dist/primer.d.ts +12 -0
- package/dist/primer.js +32 -0
- package/dist/proxyClient.d.ts +72 -0
- package/dist/proxyClient.js +193 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.js +38 -0
- package/dist/serviceGuidance.d.ts +15 -0
- package/dist/serviceGuidance.js +170 -0
- package/dist/stdio.d.ts +2 -0
- package/dist/stdio.js +107 -0
- package/dist/tools.d.ts +39 -0
- package/dist/tools.js +401 -0
- package/package.json +35 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export interface ProxyFetch {
|
|
2
|
+
(url: string, init: {
|
|
3
|
+
method: string;
|
|
4
|
+
headers: Record<string, string>;
|
|
5
|
+
body?: string;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
ok: boolean;
|
|
9
|
+
status: number;
|
|
10
|
+
json(): Promise<unknown>;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
export interface EvolverProxyClientOptions {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
token: string;
|
|
16
|
+
fetchFn?: ProxyFetch;
|
|
17
|
+
reloadSettings?: () => EvolverProxyClientOptions | undefined;
|
|
18
|
+
}
|
|
19
|
+
export interface ProxySearchArgs {
|
|
20
|
+
text?: string;
|
|
21
|
+
signalsAny?: string[];
|
|
22
|
+
kind?: string;
|
|
23
|
+
category?: string;
|
|
24
|
+
gene?: string;
|
|
25
|
+
limit?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ProxyFetchArgs {
|
|
28
|
+
assetId?: string;
|
|
29
|
+
assetIds?: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface ProxyAssetBundle {
|
|
32
|
+
assets: unknown[];
|
|
33
|
+
}
|
|
34
|
+
export interface ProxyReuseResultArgs {
|
|
35
|
+
assetId: string;
|
|
36
|
+
outcome: 'success' | 'failed' | 'mismatched' | 'stale' | 'unsafe';
|
|
37
|
+
taskId?: string;
|
|
38
|
+
traceId?: string;
|
|
39
|
+
/** Deprecated compatibility field. Scalar self-reported savings are not forwarded as audited ROI. */
|
|
40
|
+
tokensSaved?: number;
|
|
41
|
+
timeSavedSeconds?: number;
|
|
42
|
+
reason?: string;
|
|
43
|
+
}
|
|
44
|
+
export declare class EvolverProxyClient {
|
|
45
|
+
private baseUrl;
|
|
46
|
+
private token;
|
|
47
|
+
private readonly fetchFn;
|
|
48
|
+
private readonly reloadSettings;
|
|
49
|
+
constructor(opts: EvolverProxyClientOptions);
|
|
50
|
+
status(opts?: {
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}): Promise<unknown>;
|
|
53
|
+
search(args: ProxySearchArgs): Promise<unknown>;
|
|
54
|
+
fetchAsset(args: ProxyFetchArgs): Promise<unknown>;
|
|
55
|
+
submitAsset(asset: unknown): Promise<unknown>;
|
|
56
|
+
/** Pre-publish dry-run: the hub runs its quality + content-safety gate but stores nothing and charges no credits. */
|
|
57
|
+
validateAsset(asset: unknown): Promise<unknown>;
|
|
58
|
+
validateAssetBundle(bundle: ProxyAssetBundle): Promise<unknown>;
|
|
59
|
+
distillConversation(input: unknown): Promise<unknown>;
|
|
60
|
+
recordReuseResult(args: ProxyReuseResultArgs): Promise<unknown>;
|
|
61
|
+
call(method: string, path: string, body?: unknown, opts?: {
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
}): Promise<unknown>;
|
|
64
|
+
private callOnce;
|
|
65
|
+
private reloadFromSettings;
|
|
66
|
+
private proxyError;
|
|
67
|
+
}
|
|
68
|
+
export declare function proxyClientFromEnv(env?: Record<string, string | undefined>): EvolverProxyClient | undefined;
|
|
69
|
+
export declare function reachableProxyClientFromEnv(env?: Record<string, string | undefined>, opts?: {
|
|
70
|
+
fetchFn?: ProxyFetch;
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
}): Promise<EvolverProxyClient | undefined>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { lstatSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export class EvolverProxyClient {
|
|
5
|
+
baseUrl;
|
|
6
|
+
token;
|
|
7
|
+
fetchFn;
|
|
8
|
+
reloadSettings;
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
|
11
|
+
this.token = opts.token;
|
|
12
|
+
this.fetchFn = opts.fetchFn ?? globalFetch;
|
|
13
|
+
this.reloadSettings = opts.reloadSettings;
|
|
14
|
+
}
|
|
15
|
+
status(opts = {}) {
|
|
16
|
+
return this.call('GET', '/proxy/status', undefined, opts);
|
|
17
|
+
}
|
|
18
|
+
search(args) {
|
|
19
|
+
return this.call('POST', '/asset/search', {
|
|
20
|
+
...(args.text ? { text: args.text } : {}),
|
|
21
|
+
...(args.signalsAny && args.signalsAny.length > 0 ? { signals: args.signalsAny } : {}),
|
|
22
|
+
...(args.kind ? { kind: args.kind } : {}),
|
|
23
|
+
...(args.category ? { category: args.category } : {}),
|
|
24
|
+
...(args.gene ? { gene: args.gene } : {}),
|
|
25
|
+
...(args.limit !== undefined ? { limit: args.limit } : {}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
fetchAsset(args) {
|
|
29
|
+
return this.call('POST', '/asset/fetch', {
|
|
30
|
+
...(args.assetId ? { asset_id: args.assetId } : {}),
|
|
31
|
+
...(args.assetIds ? { asset_ids: args.assetIds } : {}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
submitAsset(asset) {
|
|
35
|
+
return this.call('POST', '/asset/submit', { assets: [asset] });
|
|
36
|
+
}
|
|
37
|
+
/** Pre-publish dry-run: the hub runs its quality + content-safety gate but stores nothing and charges no credits. */
|
|
38
|
+
validateAsset(asset) {
|
|
39
|
+
return this.validateAssetBundle({ assets: [asset] });
|
|
40
|
+
}
|
|
41
|
+
validateAssetBundle(bundle) {
|
|
42
|
+
return this.call('POST', '/asset/validate', bundle);
|
|
43
|
+
}
|
|
44
|
+
distillConversation(input) {
|
|
45
|
+
return this.call('POST', '/conversation/distill', input);
|
|
46
|
+
}
|
|
47
|
+
recordReuseResult(args) {
|
|
48
|
+
return this.call('POST', '/asset/reuse-result', {
|
|
49
|
+
asset_id: args.assetId,
|
|
50
|
+
outcome: args.outcome,
|
|
51
|
+
...(args.taskId ? { task_id: args.taskId } : {}),
|
|
52
|
+
...(args.traceId ? { trace_id: args.traceId } : {}),
|
|
53
|
+
...(args.timeSavedSeconds !== undefined ? { time_saved_seconds: args.timeSavedSeconds } : {}),
|
|
54
|
+
...(args.reason ? { reason: args.reason } : {}),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async call(method, path, body, opts = {}) {
|
|
58
|
+
try {
|
|
59
|
+
const result = await this.callOnce(method, path, body, opts);
|
|
60
|
+
if (result.ok)
|
|
61
|
+
return result.parsed;
|
|
62
|
+
if (result.status === 401 && this.reloadFromSettings()) {
|
|
63
|
+
const retry = await this.callOnce(method, path, body, opts);
|
|
64
|
+
if (retry.ok)
|
|
65
|
+
return retry.parsed;
|
|
66
|
+
throw this.proxyError(retry, path);
|
|
67
|
+
}
|
|
68
|
+
throw this.proxyError(result, path);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (this.reloadFromSettings()) {
|
|
72
|
+
const retry = await this.callOnce(method, path, body, opts);
|
|
73
|
+
if (retry.ok)
|
|
74
|
+
return retry.parsed;
|
|
75
|
+
throw this.proxyError(retry, path);
|
|
76
|
+
}
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async callOnce(method, path, body, opts) {
|
|
81
|
+
const res = await this.fetchFn(`${this.baseUrl}${path}`, {
|
|
82
|
+
method,
|
|
83
|
+
headers: {
|
|
84
|
+
authorization: `Bearer ${this.token}`,
|
|
85
|
+
'content-type': 'application/json',
|
|
86
|
+
},
|
|
87
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
88
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
89
|
+
});
|
|
90
|
+
return { ok: res.ok, status: res.status, parsed: await res.json() };
|
|
91
|
+
}
|
|
92
|
+
reloadFromSettings() {
|
|
93
|
+
const next = this.reloadSettings?.();
|
|
94
|
+
if (!next)
|
|
95
|
+
return false;
|
|
96
|
+
const nextBaseUrl = next.baseUrl.replace(/\/+$/, '');
|
|
97
|
+
if (nextBaseUrl === this.baseUrl && next.token === this.token)
|
|
98
|
+
return false;
|
|
99
|
+
this.baseUrl = nextBaseUrl;
|
|
100
|
+
this.token = next.token;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
proxyError(result, path) {
|
|
104
|
+
const message = result.parsed && typeof result.parsed === 'object' && !Array.isArray(result.parsed) && typeof result.parsed.error === 'string'
|
|
105
|
+
? result.parsed.error
|
|
106
|
+
: `evolver proxy ${result.status} ${path}`;
|
|
107
|
+
return new Error(message);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function proxyClientFromEnv(env = process.env) {
|
|
111
|
+
const token = env['EVOLVER_IPC_TOKEN']?.trim();
|
|
112
|
+
if (!token)
|
|
113
|
+
return proxyClientFromSettings(env, env === process.env);
|
|
114
|
+
const explicitUrl = env['EVOLVER_PROXY_URL']?.trim();
|
|
115
|
+
const port = env['EVOLVER_IPC_PORT']?.trim() || env['EVOMAP_PROXY_PORT']?.trim() || '19820';
|
|
116
|
+
return new EvolverProxyClient({ baseUrl: explicitUrl || `http://127.0.0.1:${port}`, token });
|
|
117
|
+
}
|
|
118
|
+
export async function reachableProxyClientFromEnv(env = process.env, opts = {}) {
|
|
119
|
+
const token = env['EVOLVER_IPC_TOKEN']?.trim();
|
|
120
|
+
if (token) {
|
|
121
|
+
const explicitUrl = env['EVOLVER_PROXY_URL']?.trim();
|
|
122
|
+
const port = env['EVOLVER_IPC_PORT']?.trim() || env['EVOMAP_PROXY_PORT']?.trim() || '19820';
|
|
123
|
+
return new EvolverProxyClient({ baseUrl: explicitUrl || `http://127.0.0.1:${port}`, token, ...(opts.fetchFn ? { fetchFn: opts.fetchFn } : {}) });
|
|
124
|
+
}
|
|
125
|
+
const client = proxyClientFromSettings(env, env === process.env, opts.fetchFn);
|
|
126
|
+
if (!client)
|
|
127
|
+
return undefined;
|
|
128
|
+
return await proxyClientReachable(client, opts.timeoutMs ?? 250) ? client : undefined;
|
|
129
|
+
}
|
|
130
|
+
function proxyClientFromSettings(env, allowDefaultHome, fetchFn) {
|
|
131
|
+
const settings = readProxySettings(env, allowDefaultHome);
|
|
132
|
+
return settings ? new EvolverProxyClient({
|
|
133
|
+
...settings,
|
|
134
|
+
...(fetchFn ? { fetchFn } : {}),
|
|
135
|
+
reloadSettings: () => readProxySettings(env, allowDefaultHome),
|
|
136
|
+
}) : undefined;
|
|
137
|
+
}
|
|
138
|
+
function readProxySettings(env, allowDefaultHome) {
|
|
139
|
+
const homeDir = env['HOME']?.trim() || (allowDefaultHome ? homedir() : '');
|
|
140
|
+
if (!homeDir)
|
|
141
|
+
return undefined;
|
|
142
|
+
const settingsPath = join(homeDir, '.evolver', 'settings.json');
|
|
143
|
+
try {
|
|
144
|
+
if (!lstatSync(settingsPath).isFile())
|
|
145
|
+
return undefined;
|
|
146
|
+
const parsed = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
147
|
+
const proxy = recordValue(recordValue(parsed)['proxy']);
|
|
148
|
+
const baseUrl = typeof proxy['url'] === 'string' ? proxy['url'].trim() : '';
|
|
149
|
+
const token = typeof proxy['token'] === 'string' ? proxy['token'].trim() : '';
|
|
150
|
+
if (!baseUrl || !token || !isLoopbackHttpUrl(baseUrl))
|
|
151
|
+
return undefined;
|
|
152
|
+
return { baseUrl, token };
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function isLoopbackHttpUrl(raw) {
|
|
159
|
+
try {
|
|
160
|
+
const url = new URL(raw);
|
|
161
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
162
|
+
return false;
|
|
163
|
+
const hostname = url.hostname.toLowerCase();
|
|
164
|
+
return hostname === '127.0.0.1'
|
|
165
|
+
|| hostname === 'localhost'
|
|
166
|
+
|| hostname === '[::1]'
|
|
167
|
+
|| hostname === '::1';
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function recordValue(value) {
|
|
174
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
175
|
+
}
|
|
176
|
+
async function proxyClientReachable(client, timeoutMs) {
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
|
|
179
|
+
try {
|
|
180
|
+
await client.status({ signal: controller.signal });
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
if (timer)
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function globalFetch(url, init) {
|
|
192
|
+
return fetch(url, init);
|
|
193
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { McpTool } from './tools.js';
|
|
2
|
+
export interface ToolListEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface ToolCallResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
result?: unknown;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class UnknownToolError extends Error {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
constructor(name: string);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Evolver MCP server 核心(M5-1). 与传输无关(stdio 适配器薄薄一层包它),
|
|
18
|
+
* 便于直接单测; listTools 给 agent 自然发现, callTool 分派 + 隔离错误.
|
|
19
|
+
*/
|
|
20
|
+
export declare class EvolverMcpServer {
|
|
21
|
+
private readonly tools;
|
|
22
|
+
/** Server-level onboarding text (#mcp-onboarding). A transport surfaces it as the MCP `initialize.instructions`
|
|
23
|
+
* field so any connecting client hands the evolver mechanism to its model. Empty string when not provided. */
|
|
24
|
+
readonly instructions: string;
|
|
25
|
+
constructor(tools: readonly McpTool[], opts?: {
|
|
26
|
+
instructions?: string;
|
|
27
|
+
});
|
|
28
|
+
listTools(): ToolListEntry[];
|
|
29
|
+
callTool(name: string, args?: Record<string, unknown>): Promise<ToolCallResult>;
|
|
30
|
+
has(name: string): boolean;
|
|
31
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class UnknownToolError extends Error {
|
|
2
|
+
name;
|
|
3
|
+
constructor(name) {
|
|
4
|
+
super(`未知 MCP 工具: ${name}`);
|
|
5
|
+
this.name = name;
|
|
6
|
+
this.name = 'UnknownToolError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Evolver MCP server 核心(M5-1). 与传输无关(stdio 适配器薄薄一层包它),
|
|
11
|
+
* 便于直接单测; listTools 给 agent 自然发现, callTool 分派 + 隔离错误.
|
|
12
|
+
*/
|
|
13
|
+
export class EvolverMcpServer {
|
|
14
|
+
tools = new Map();
|
|
15
|
+
/** Server-level onboarding text (#mcp-onboarding). A transport surfaces it as the MCP `initialize.instructions`
|
|
16
|
+
* field so any connecting client hands the evolver mechanism to its model. Empty string when not provided. */
|
|
17
|
+
instructions;
|
|
18
|
+
constructor(tools, opts = {}) {
|
|
19
|
+
for (const t of tools)
|
|
20
|
+
this.tools.set(t.name, t);
|
|
21
|
+
this.instructions = opts.instructions ?? '';
|
|
22
|
+
}
|
|
23
|
+
listTools() {
|
|
24
|
+
return [...this.tools.values()].map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }));
|
|
25
|
+
}
|
|
26
|
+
async callTool(name, args = {}) {
|
|
27
|
+
const tool = this.tools.get(name);
|
|
28
|
+
if (!tool)
|
|
29
|
+
throw new UnknownToolError(name);
|
|
30
|
+
try {
|
|
31
|
+
return { ok: true, result: await tool.handler(args) };
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
has(name) { return this.tools.has(name); }
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { McpServerCmd } from './injection.js';
|
|
2
|
+
export type ServiceTarget = 'launchd' | 'systemd' | 'windows' | 'compose' | 'k8s';
|
|
3
|
+
export declare const SERVICE_TARGETS: readonly ServiceTarget[];
|
|
4
|
+
export interface ServiceGuidanceContext {
|
|
5
|
+
/** Path to the credential store the service should reference via EVOLVER_ENV_FILE. Falls back to the exec env's
|
|
6
|
+
* pointer, else a placeholder. NEVER a secret value — only the pointer path. */
|
|
7
|
+
envFile?: string;
|
|
8
|
+
/** The long-running evolver command the service runs (illustrative — operator may swap for autoexec/proxy/etc). */
|
|
9
|
+
exec: McpServerCmd;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Render a service template for `target`. Print-only; the operator edits + installs it. Each template wires the
|
|
13
|
+
* credential store via the EVOLVER_ENV_FILE pointer (never inlines a secret) and runs the given evolver command.
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderServiceGuidance(target: ServiceTarget, ctx: ServiceGuidanceContext): string;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export const SERVICE_TARGETS = ['launchd', 'systemd', 'windows', 'compose', 'k8s'];
|
|
2
|
+
const ENV_FILE_KEY = 'EVOLVER_ENV_FILE';
|
|
3
|
+
const execParts = (exec) => [exec.command, ...(exec.args ?? [])];
|
|
4
|
+
const cmdLine = (exec) => execParts(exec).map(quotePosix).join(' ');
|
|
5
|
+
const envPathOf = (ctx) => ctx.envFile ?? ctx.exec.env?.[ENV_FILE_KEY] ?? '<path-to-evolver.env>';
|
|
6
|
+
/**
|
|
7
|
+
* Render a service template for `target`. Print-only; the operator edits + installs it. Each template wires the
|
|
8
|
+
* credential store via the EVOLVER_ENV_FILE pointer (never inlines a secret) and runs the given evolver command.
|
|
9
|
+
*/
|
|
10
|
+
export function renderServiceGuidance(target, ctx) {
|
|
11
|
+
const cmd = cmdLine(ctx.exec);
|
|
12
|
+
const ef = envPathOf(ctx);
|
|
13
|
+
switch (target) {
|
|
14
|
+
case 'systemd':
|
|
15
|
+
return [
|
|
16
|
+
'# Linux systemd user unit — ~/.config/systemd/user/evolver-proxy.service',
|
|
17
|
+
'[Unit]',
|
|
18
|
+
'Description=EvoMap Evolver Proxy Daemon',
|
|
19
|
+
'After=network-online.target',
|
|
20
|
+
'Wants=network-online.target',
|
|
21
|
+
'StartLimitBurst=5',
|
|
22
|
+
'StartLimitIntervalSec=120s',
|
|
23
|
+
'',
|
|
24
|
+
'[Service]',
|
|
25
|
+
'Type=simple',
|
|
26
|
+
`Environment="${ENV_FILE_KEY}=${escapeSystemdEnvValue(ef)}"`,
|
|
27
|
+
`ExecStart=${execParts(ctx.exec).map(quoteSystemdArg).join(' ')}`,
|
|
28
|
+
'Restart=on-failure',
|
|
29
|
+
'RestartSec=5s',
|
|
30
|
+
'RestartPreventExitStatus=0',
|
|
31
|
+
'RestartForceExitStatus=78',
|
|
32
|
+
'TimeoutStopSec=30s',
|
|
33
|
+
'StandardOutput=journal',
|
|
34
|
+
'StandardError=journal',
|
|
35
|
+
'SyslogIdentifier=evolver-proxy',
|
|
36
|
+
'NoNewPrivileges=true',
|
|
37
|
+
'PrivateTmp=true',
|
|
38
|
+
'',
|
|
39
|
+
'[Install]',
|
|
40
|
+
'WantedBy=default.target',
|
|
41
|
+
'',
|
|
42
|
+
'# then: systemctl --user daemon-reload && systemctl --user enable --now evolver-proxy',
|
|
43
|
+
`# ${ENV_FILE_KEY} is a pointer to the credential store; never inline secret values in this unit.`,
|
|
44
|
+
].join('\n');
|
|
45
|
+
case 'launchd':
|
|
46
|
+
return [
|
|
47
|
+
'<!-- macOS launchd — ~/Library/LaunchAgents/com.evomap.evolver-proxy.plist -->',
|
|
48
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
49
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
50
|
+
'<plist version="1.0">',
|
|
51
|
+
'<dict>',
|
|
52
|
+
' <key>Label</key>',
|
|
53
|
+
' <string>com.evomap.evolver-proxy</string>',
|
|
54
|
+
' <key>EnvironmentVariables</key>',
|
|
55
|
+
' <dict>',
|
|
56
|
+
` <key>${ENV_FILE_KEY}</key>`,
|
|
57
|
+
` <string>${escapeXml(ef)}</string>`,
|
|
58
|
+
' <key>PATH</key>',
|
|
59
|
+
' <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>',
|
|
60
|
+
' </dict>',
|
|
61
|
+
' <key>ProgramArguments</key>',
|
|
62
|
+
' <array>',
|
|
63
|
+
...execParts(ctx.exec).map((a) => ` <string>${escapeXml(a)}</string>`),
|
|
64
|
+
' </array>',
|
|
65
|
+
' <key>RunAtLoad</key>',
|
|
66
|
+
' <true/>',
|
|
67
|
+
' <key>KeepAlive</key>',
|
|
68
|
+
' <dict>',
|
|
69
|
+
' <key>SuccessfulExit</key>',
|
|
70
|
+
' <false/>',
|
|
71
|
+
' </dict>',
|
|
72
|
+
' <key>ThrottleInterval</key>',
|
|
73
|
+
' <integer>5</integer>',
|
|
74
|
+
' <key>ProcessType</key>',
|
|
75
|
+
' <string>Standard</string>',
|
|
76
|
+
'</dict>',
|
|
77
|
+
'</plist>',
|
|
78
|
+
'',
|
|
79
|
+
'# then: launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.evomap.evolver-proxy.plist',
|
|
80
|
+
`# (the pointer ${ENV_FILE_KEY}=${ef} references the credential store; never inline secrets)`,
|
|
81
|
+
].join('\n');
|
|
82
|
+
case 'windows':
|
|
83
|
+
return [
|
|
84
|
+
'# Windows Task Scheduler — hidden wscript.exe launcher; do not use a foreground command wrapper',
|
|
85
|
+
'$launcher = "$env:LOCALAPPDATA\\EvoMap\\evolver-proxy-task-launcher.vbs"',
|
|
86
|
+
'$dir = Split-Path -Parent $launcher',
|
|
87
|
+
'if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null }',
|
|
88
|
+
'$body = @\'',
|
|
89
|
+
"' AUTO-GENERATED; wscript.exe runs without a visible console window.",
|
|
90
|
+
'Dim WshShell, env, cmd, rc',
|
|
91
|
+
'Set WshShell = CreateObject("WScript.Shell")',
|
|
92
|
+
'Set env = WshShell.Environment("PROCESS")',
|
|
93
|
+
`env("${ENV_FILE_KEY}") = ${vbsString(ef)}`,
|
|
94
|
+
`cmd = ${vbsString(windowsCommandLine(ctx.exec))}`,
|
|
95
|
+
'rc = WshShell.Run(cmd, 0, True)',
|
|
96
|
+
'WScript.Quit rc',
|
|
97
|
+
"'@",
|
|
98
|
+
'Set-Content -Path $launcher -Value $body -Encoding Unicode',
|
|
99
|
+
'schtasks /Create /TN EvoMapEvolverProxyDaemon /TR "wscript.exe ""%LOCALAPPDATA%\\EvoMap\\evolver-proxy-task-launcher.vbs""" /SC ONLOGON /RL LIMITED /F',
|
|
100
|
+
`# The VBS sets only the ${ENV_FILE_KEY} pointer; the credential store stays out of the task definition.`,
|
|
101
|
+
].join('\n');
|
|
102
|
+
case 'compose':
|
|
103
|
+
return [
|
|
104
|
+
'# docker compose — mount the credential store as a file and point EVOLVER_ENV_FILE at it (do NOT bake secrets into the image/env)',
|
|
105
|
+
'services:',
|
|
106
|
+
' evolver:',
|
|
107
|
+
' image: <your-evolver-image>',
|
|
108
|
+
` command: ${cmd}`,
|
|
109
|
+
' environment:',
|
|
110
|
+
` ${ENV_FILE_KEY}: /run/secrets/evolver_env # pointer to the mounted store`,
|
|
111
|
+
' secrets:',
|
|
112
|
+
' - evolver_env',
|
|
113
|
+
' restart: on-failure',
|
|
114
|
+
'secrets:',
|
|
115
|
+
' evolver_env:',
|
|
116
|
+
` file: ${ef} # the real credential store, mounted at /run/secrets/evolver_env`,
|
|
117
|
+
].join('\n');
|
|
118
|
+
case 'k8s':
|
|
119
|
+
return [
|
|
120
|
+
'# Kubernetes — keep creds in a Secret, mount it as a file, point EVOLVER_ENV_FILE at the mount (never inline in env)',
|
|
121
|
+
'apiVersion: apps/v1',
|
|
122
|
+
'kind: Deployment',
|
|
123
|
+
'metadata: { name: evolver }',
|
|
124
|
+
'spec:',
|
|
125
|
+
' replicas: 1',
|
|
126
|
+
' selector: { matchLabels: { app: evolver } }',
|
|
127
|
+
' template:',
|
|
128
|
+
' metadata: { labels: { app: evolver } }',
|
|
129
|
+
' spec:',
|
|
130
|
+
' containers:',
|
|
131
|
+
' - name: evolver',
|
|
132
|
+
' image: <your-evolver-image>',
|
|
133
|
+
` command: [${[ctx.exec.command, ...(ctx.exec.args ?? [])].map((a) => `"${a}"`).join(', ')}]`,
|
|
134
|
+
` env: [{ name: ${ENV_FILE_KEY}, value: /etc/evolver/evolver.env }] # pointer to the mounted secret`,
|
|
135
|
+
' volumeMounts: [{ name: evolver-env, mountPath: /etc/evolver, readOnly: true }]',
|
|
136
|
+
' volumes:',
|
|
137
|
+
` - name: evolver-env`,
|
|
138
|
+
` secret: { secretName: evolver-env } # create from your store: kubectl create secret generic evolver-env --from-file=evolver.env=${ef}`,
|
|
139
|
+
].join('\n');
|
|
140
|
+
default: {
|
|
141
|
+
const _exhaustive = target;
|
|
142
|
+
throw new Error(`unknown service target: ${String(_exhaustive)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function quotePosix(value) {
|
|
147
|
+
return /^[A-Za-z0-9_/:.@%+=,-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
148
|
+
}
|
|
149
|
+
function quoteSystemdArg(value) {
|
|
150
|
+
return /^[A-Za-z0-9_/:.@%+=,-]+$/.test(value)
|
|
151
|
+
? value
|
|
152
|
+
: `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('$', '\\$')}"`;
|
|
153
|
+
}
|
|
154
|
+
function escapeSystemdEnvValue(value) {
|
|
155
|
+
return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('$', '\\$');
|
|
156
|
+
}
|
|
157
|
+
function escapeXml(value) {
|
|
158
|
+
return value
|
|
159
|
+
.replaceAll('&', '&')
|
|
160
|
+
.replaceAll('<', '<')
|
|
161
|
+
.replaceAll('>', '>')
|
|
162
|
+
.replaceAll('"', '"')
|
|
163
|
+
.replaceAll("'", ''');
|
|
164
|
+
}
|
|
165
|
+
function windowsCommandLine(exec) {
|
|
166
|
+
return execParts(exec).map((part) => `"${part.replaceAll('"', '""')}"`).join(' ');
|
|
167
|
+
}
|
|
168
|
+
function vbsString(value) {
|
|
169
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
170
|
+
}
|
package/dist/stdio.d.ts
ADDED
package/dist/stdio.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { mkdirSync } from 'node:fs';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { assetstore, events, mailbox } from '@evomap/evolver-core';
|
|
7
|
+
import { buildEvolverTools } from './tools.js';
|
|
8
|
+
import { buildEvolverPrimer } from './primer.js';
|
|
9
|
+
import { EvolverMcpServer, UnknownToolError } from './server.js';
|
|
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`);
|
|
15
|
+
}
|
|
16
|
+
const store = new assetstore.LocalJsonlProvider(events.assetsDir());
|
|
17
|
+
const mailboxPath = process.env['EVOLVER_MCP_MAILBOX'] ?? join(events.evomapHome(), 'mailbox', 'mcp.db');
|
|
18
|
+
mkdirSync(dirname(mailboxPath), { recursive: true });
|
|
19
|
+
const box = new mailbox.MailboxStore({ path: mailboxPath });
|
|
20
|
+
const proxy = await reachableProxyClientFromEnv(process.env);
|
|
21
|
+
// Reuse-feedback wiring (#268): a root_events writer + a per-connection correlation id so a SUCCESS reuse_result
|
|
22
|
+
// from THIS MCP agent credits the local experience loop (one stdio process ~ one MCP session).
|
|
23
|
+
const ingestor = new events.Ingestor({ path: events.rootEventsPath() });
|
|
24
|
+
const connId = `mcp-${randomUUID()}`;
|
|
25
|
+
const server = new EvolverMcpServer(buildEvolverTools({ store, mailbox: box, ingestor, cycleId: connId, ...(proxy ? { proxy } : {}) }), { instructions: buildEvolverPrimer({ proxy: !!proxy }) });
|
|
26
|
+
function send(message) {
|
|
27
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
28
|
+
}
|
|
29
|
+
function ok(id, result) {
|
|
30
|
+
send({ jsonrpc: '2.0', id, result });
|
|
31
|
+
}
|
|
32
|
+
function fail(id, code, message) {
|
|
33
|
+
send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
34
|
+
}
|
|
35
|
+
function asRecord(value) {
|
|
36
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
37
|
+
}
|
|
38
|
+
async function handle(req) {
|
|
39
|
+
const id = req.id ?? null;
|
|
40
|
+
const method = req.method;
|
|
41
|
+
if (!method)
|
|
42
|
+
return fail(id, -32600, 'missing method');
|
|
43
|
+
if (method === 'initialize') {
|
|
44
|
+
const params = asRecord(req.params);
|
|
45
|
+
ok(id, {
|
|
46
|
+
protocolVersion: typeof params['protocolVersion'] === 'string' ? params['protocolVersion'] : '2024-11-05',
|
|
47
|
+
capabilities: { tools: {} },
|
|
48
|
+
serverInfo: { name: 'evolver-mcp', version: '0.0.0' },
|
|
49
|
+
// Quiet mechanism primer (#mcp-onboarding): keep tool workflow available without encouraging user-visible
|
|
50
|
+
// narration of routine Evolver checks.
|
|
51
|
+
...(server.instructions ? { instructions: server.instructions } : {}),
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (method === 'notifications/initialized')
|
|
56
|
+
return;
|
|
57
|
+
if (method === 'ping')
|
|
58
|
+
return ok(id, {});
|
|
59
|
+
if (method === 'tools/list')
|
|
60
|
+
return ok(id, { tools: server.listTools() });
|
|
61
|
+
if (method === 'tools/call') {
|
|
62
|
+
const params = asRecord(req.params);
|
|
63
|
+
const name = typeof params['name'] === 'string' ? params['name'] : '';
|
|
64
|
+
const args = asRecord(params['arguments']);
|
|
65
|
+
try {
|
|
66
|
+
const r = await server.callTool(name, args);
|
|
67
|
+
ok(id, {
|
|
68
|
+
content: [{ type: 'text', text: r.ok ? JSON.stringify(r.result, null, 2) : (r.error ?? 'tool failed') }],
|
|
69
|
+
isError: !r.ok,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const message = err instanceof UnknownToolError ? err.message : (err instanceof Error ? err.message : String(err));
|
|
74
|
+
fail(id, err instanceof UnknownToolError ? -32601 : -32603, message);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
fail(id, -32601, `unknown method: ${method}`);
|
|
79
|
+
}
|
|
80
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
81
|
+
let requestQueue = Promise.resolve();
|
|
82
|
+
async function processLine(line) {
|
|
83
|
+
const raw = line.trim();
|
|
84
|
+
if (!raw)
|
|
85
|
+
return;
|
|
86
|
+
let req;
|
|
87
|
+
try {
|
|
88
|
+
req = JSON.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
fail(null, -32700, err instanceof Error ? err.message : String(err));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await handle(req);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
fail(req.id ?? null, -32603, err instanceof Error ? err.message : String(err));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
rl.on('line', (line) => {
|
|
102
|
+
const next = requestQueue.then(() => processLine(line), () => processLine(line));
|
|
103
|
+
requestQueue = next.catch(() => undefined);
|
|
104
|
+
});
|
|
105
|
+
rl.on('close', () => { void requestQueue.finally(() => { box.close(); process.exit(0); }); });
|
|
106
|
+
process.on('SIGINT', () => { box.close(); process.exit(0); });
|
|
107
|
+
process.on('SIGTERM', () => { box.close(); process.exit(0); });
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { assetstore, mailbox as mb } from '@evomap/evolver-core';
|
|
2
|
+
import type { EvolverProxyClient } from './proxyClient.js';
|
|
3
|
+
/** Minimal root_events writer the reuse-feedback path needs (#268). Structural so the stdio server can pass the
|
|
4
|
+
* real `events.Ingestor` and tests can pass a fake — tools.ts stays decoupled from the concrete class. */
|
|
5
|
+
export interface ReuseHitIngestor {
|
|
6
|
+
ingest(raw: {
|
|
7
|
+
type: string;
|
|
8
|
+
human: {
|
|
9
|
+
title: string;
|
|
10
|
+
detail?: string;
|
|
11
|
+
};
|
|
12
|
+
payload?: Record<string, unknown>;
|
|
13
|
+
}): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
/** MCP 工具(自带描述, agent runtime 在 tool list 自然发现, 按需调用 — 优于扔大 skill/--help). */
|
|
16
|
+
export interface McpTool {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
inputSchema: Record<string, unknown>;
|
|
20
|
+
handler: (args: Record<string, unknown>) => Promise<unknown>;
|
|
21
|
+
}
|
|
22
|
+
export interface EvolverToolDeps {
|
|
23
|
+
store: assetstore.AssetStoreProvider;
|
|
24
|
+
mailbox?: mb.MailboxStore;
|
|
25
|
+
proxy?: EvolverProxyClient;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
/** Root_events writer for the MCP reuse-feedback loop (#268). When wired, a SUCCESS reuse-result emits a local
|
|
28
|
+
* `value.reuse_hit` so the local ledger credits reuse driven by an MCP agent (not just the hook/daemon path).
|
|
29
|
+
* Absent → no local emission (the tool still works, just no local feedback). */
|
|
30
|
+
ingestor?: ReuseHitIngestor;
|
|
31
|
+
/** Server-minted correlation id for this MCP connection (#268). Used as the reuse_hit `cycleId` audit anchor and,
|
|
32
|
+
* with the agent's optional taskId, as the idempotency key. One stdio process ~ one MCP session. */
|
|
33
|
+
cycleId?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Evolver MCP 工具集(M5-2). asset.search/fetch/publish + gep.build + mailbox.*.
|
|
37
|
+
* schema 单一来源走 gep-sdk(经 evolver-core 重导出), 不重复实现.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildEvolverTools(deps: EvolverToolDeps): McpTool[];
|