imprint-mcp 0.2.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/CHANGELOG.md +168 -0
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/examples/discoverandgo/README.md +57 -0
- package/examples/discoverandgo/book_discoverandgo_museum_pass/cron.json +8 -0
- package/examples/discoverandgo/book_discoverandgo_museum_pass/index.ts +89 -0
- package/examples/discoverandgo/book_discoverandgo_museum_pass/workflow.json +39 -0
- package/examples/echo/README.md +37 -0
- package/examples/echo/echo_test/index.ts +31 -0
- package/examples/google-flights/search_google_flights/index.ts +101 -0
- package/examples/google-flights/search_google_flights/parser.test.ts +140 -0
- package/examples/google-flights/search_google_flights/parser.ts +189 -0
- package/examples/google-flights/search_google_flights/playbook.yaml +130 -0
- package/examples/google-flights/search_google_flights/workflow.json +48 -0
- package/examples/google-hotels/search_google_hotels/index.ts +194 -0
- package/examples/google-hotels/search_google_hotels/parser.test.ts +168 -0
- package/examples/google-hotels/search_google_hotels/parser.ts +330 -0
- package/examples/google-hotels/search_google_hotels/playbook.yaml +125 -0
- package/examples/google-hotels/search_google_hotels/workflow.json +111 -0
- package/examples/namecheap-domains/search_namecheap_domains/index.ts +144 -0
- package/examples/namecheap-domains/search_namecheap_domains/parser.ts +380 -0
- package/examples/namecheap-domains/search_namecheap_domains/playbook.yaml +50 -0
- package/examples/namecheap-domains/search_namecheap_domains/request-transform.ts +136 -0
- package/examples/namecheap-domains/search_namecheap_domains/workflow.json +97 -0
- package/examples/southwest/README.md +81 -0
- package/examples/southwest/search_southwest_flights/backends.json +23 -0
- package/examples/southwest/search_southwest_flights/cron.json +19 -0
- package/examples/southwest/search_southwest_flights/index.ts +110 -0
- package/examples/southwest/search_southwest_flights/playbook.yaml +46 -0
- package/examples/southwest/search_southwest_flights/workflow.json +54 -0
- package/package.json +78 -0
- package/prompts/compile-agent.md +580 -0
- package/prompts/intent-detection.md +198 -0
- package/prompts/playbook-compilation.md +279 -0
- package/prompts/request-triage.md +74 -0
- package/prompts/tool-candidate-detection.md +104 -0
- package/src/cli.ts +1287 -0
- package/src/imprint/agent.ts +468 -0
- package/src/imprint/app-api-hosts.ts +53 -0
- package/src/imprint/backend-ladder.ts +568 -0
- package/src/imprint/check.ts +136 -0
- package/src/imprint/chromium.ts +211 -0
- package/src/imprint/claude-cli-compile.ts +640 -0
- package/src/imprint/cli-credential.ts +394 -0
- package/src/imprint/codex-cli-compile.ts +712 -0
- package/src/imprint/compile-agent-types.ts +40 -0
- package/src/imprint/compile-agent.ts +404 -0
- package/src/imprint/compile-tools.ts +1389 -0
- package/src/imprint/compile.ts +720 -0
- package/src/imprint/cookie-jar.ts +246 -0
- package/src/imprint/credential-bundle.ts +195 -0
- package/src/imprint/credential-extract.ts +290 -0
- package/src/imprint/credential-store.ts +707 -0
- package/src/imprint/cron.ts +312 -0
- package/src/imprint/doctor.ts +223 -0
- package/src/imprint/emit.ts +154 -0
- package/src/imprint/etld.ts +134 -0
- package/src/imprint/freeform-redact.ts +216 -0
- package/src/imprint/inject-listener.ts +137 -0
- package/src/imprint/install.ts +795 -0
- package/src/imprint/integrations.ts +385 -0
- package/src/imprint/is-compiled.ts +2 -0
- package/src/imprint/json-path.ts +100 -0
- package/src/imprint/llm.ts +998 -0
- package/src/imprint/load-json.ts +54 -0
- package/src/imprint/log.ts +33 -0
- package/src/imprint/login.ts +166 -0
- package/src/imprint/mcp-compile-server.ts +282 -0
- package/src/imprint/mcp-maintenance.ts +1790 -0
- package/src/imprint/mcp-server.ts +350 -0
- package/src/imprint/multi-progress.ts +69 -0
- package/src/imprint/notify.ts +155 -0
- package/src/imprint/paths.ts +64 -0
- package/src/imprint/playbook-parser.ts +21 -0
- package/src/imprint/playbook-runner.ts +465 -0
- package/src/imprint/probe-backends.ts +251 -0
- package/src/imprint/progress.ts +28 -0
- package/src/imprint/record.ts +470 -0
- package/src/imprint/redact.ts +550 -0
- package/src/imprint/replay-capture.ts +387 -0
- package/src/imprint/request-context.ts +66 -0
- package/src/imprint/runtime-link.ts +73 -0
- package/src/imprint/runtime.ts +942 -0
- package/src/imprint/sensitive-keys.ts +156 -0
- package/src/imprint/session-diff.ts +409 -0
- package/src/imprint/session-merge.ts +198 -0
- package/src/imprint/session-writer.ts +149 -0
- package/src/imprint/sites.ts +27 -0
- package/src/imprint/stealth-fetch.ts +434 -0
- package/src/imprint/teach-state.ts +235 -0
- package/src/imprint/teach.ts +2120 -0
- package/src/imprint/tool-candidates.ts +423 -0
- package/src/imprint/tool-loader.ts +186 -0
- package/src/imprint/tool-selection.ts +70 -0
- package/src/imprint/tracing.ts +508 -0
- package/src/imprint/types.ts +472 -0
- package/src/imprint/version.ts +21 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load + JSON-parse + schema-validate a config file. Errors at each step
|
|
3
|
+
* include a specific remediation hint so the user knows what to fix.
|
|
4
|
+
*
|
|
5
|
+
* Used by: cron.ts (cron.json), emit.ts (workflow.json), cli.ts redact +
|
|
6
|
+
* compile.ts (session.json). Before this helper each verb hand-rolled
|
|
7
|
+
* the same three-branch error format.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
11
|
+
import type { ZodTypeAny, z } from 'zod';
|
|
12
|
+
|
|
13
|
+
interface LoadJsonRemediation {
|
|
14
|
+
/** What to suggest when the file doesn't exist. Should be one or more lines, each starting with "→". */
|
|
15
|
+
notFound: string;
|
|
16
|
+
/** Suggestion when JSON.parse throws. Optional. Same format. */
|
|
17
|
+
notJson?: string;
|
|
18
|
+
/** Suggestion when schema validation fails. Optional. Same format. */
|
|
19
|
+
badSchema?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Read JSON from disk, validate against `schema`. Throws Error with
|
|
23
|
+
* a multi-line message on any failure. Returns the schema's *output*
|
|
24
|
+
* type (post-defaults), not the input type. */
|
|
25
|
+
export function loadJsonFile<S extends ZodTypeAny>(
|
|
26
|
+
path: string,
|
|
27
|
+
schema: S,
|
|
28
|
+
remediation: LoadJsonRemediation,
|
|
29
|
+
noun = 'file',
|
|
30
|
+
): z.infer<S> {
|
|
31
|
+
if (!existsSync(path)) {
|
|
32
|
+
throw new Error(`${noun} not found: ${path}\n${remediation.notFound}`);
|
|
33
|
+
}
|
|
34
|
+
if (!statSync(path).isFile()) {
|
|
35
|
+
throw new Error(`${noun} is not a file: ${path}\n${remediation.notFound}`);
|
|
36
|
+
}
|
|
37
|
+
let raw: unknown;
|
|
38
|
+
try {
|
|
39
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
42
|
+
const tail = remediation.notJson ? `\n${remediation.notJson}` : '';
|
|
43
|
+
throw new Error(`${path} is not valid JSON: ${msg}${tail}`);
|
|
44
|
+
}
|
|
45
|
+
const parsed = schema.safeParse(raw);
|
|
46
|
+
if (!parsed.success) {
|
|
47
|
+
const issues = parsed.error.errors
|
|
48
|
+
.map((e) => ` - ${e.path.join('.') || '(root)'}: ${e.message}`)
|
|
49
|
+
.join('\n');
|
|
50
|
+
const tail = remediation.badSchema ? `\n${remediation.badSchema}` : '';
|
|
51
|
+
throw new Error(`${path} doesn't match the ${noun} schema:\n${issues}${tail}`);
|
|
52
|
+
}
|
|
53
|
+
return parsed.data;
|
|
54
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Logger factory + env-flag helpers.
|
|
2
|
+
*
|
|
3
|
+
* createLog('cron')('hi') → stderr `[imprint cron] hi`.
|
|
4
|
+
*
|
|
5
|
+
* Suppressed entirely when IMPRINT_QUIET=1 (set by `imprint cron --quiet`
|
|
6
|
+
* for OS-scheduler-friendly silent runs). Errors should not flow through
|
|
7
|
+
* this; they should go to stderr via console.error or process.stderr.write
|
|
8
|
+
* directly so they survive --quiet.
|
|
9
|
+
*
|
|
10
|
+
* IMPRINT_DEBUG=1 enables verbose tracing in record.ts / chromium.ts.
|
|
11
|
+
* Both flags check the literal '1' value (not truthy coercion) so
|
|
12
|
+
* IMPRINT_DEBUG=0 actually disables, as the user expects. */
|
|
13
|
+
|
|
14
|
+
type Log = (msg: string) => void;
|
|
15
|
+
|
|
16
|
+
const isQuiet = (): boolean => process.env.IMPRINT_QUIET === '1';
|
|
17
|
+
export const isDebug = (): boolean => process.env.IMPRINT_DEBUG === '1';
|
|
18
|
+
|
|
19
|
+
let muted = false;
|
|
20
|
+
export function muteLog(): void {
|
|
21
|
+
muted = true;
|
|
22
|
+
}
|
|
23
|
+
export function unmuteLog(): void {
|
|
24
|
+
muted = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createLog(area: string): Log {
|
|
28
|
+
const prefix = `[imprint ${area}]`;
|
|
29
|
+
return (msg: string): void => {
|
|
30
|
+
if (isQuiet() || muted) return;
|
|
31
|
+
process.stderr.write(`${prefix} ${msg}\n`);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/** `imprint login` — extract cookies + per-site values from a captured
|
|
2
|
+
* session.json into the credential manager. */
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import {
|
|
6
|
+
type StorageRecord,
|
|
7
|
+
getCredentialBackend,
|
|
8
|
+
setManifestStorageKeys,
|
|
9
|
+
upsertManifestEntry,
|
|
10
|
+
} from './credential-store.ts';
|
|
11
|
+
import { type Session, SessionSchema } from './types.ts';
|
|
12
|
+
|
|
13
|
+
interface LoginOptions {
|
|
14
|
+
site: string;
|
|
15
|
+
/** Path to a session.json from which to extract credentials. */
|
|
16
|
+
fromSession: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface LoginResult {
|
|
20
|
+
backend: 'keyring' | 'encrypted-file' | 'legacy-json';
|
|
21
|
+
cookieCount: number;
|
|
22
|
+
storageCount: number;
|
|
23
|
+
values: Record<string, string>;
|
|
24
|
+
/** Pattern names that matched and contributed values. */
|
|
25
|
+
matchedExtractors: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function login(opts: LoginOptions): Promise<LoginResult> {
|
|
29
|
+
const raw = JSON.parse(readFileSync(opts.fromSession, 'utf8'));
|
|
30
|
+
const session: Session = SessionSchema.parse(raw);
|
|
31
|
+
|
|
32
|
+
const cookies = collectCookies(session);
|
|
33
|
+
const storage = collectStorage(session);
|
|
34
|
+
const { values, matched } = extractKnownValues(session);
|
|
35
|
+
|
|
36
|
+
const backend = await getCredentialBackend();
|
|
37
|
+
await backend.setCookies(opts.site, cookies);
|
|
38
|
+
if (backend.setStorage) {
|
|
39
|
+
await backend.setStorage(opts.site, storage);
|
|
40
|
+
setManifestStorageKeys(
|
|
41
|
+
opts.site,
|
|
42
|
+
storage.map((s) => ({ origin: s.origin, kind: s.kind, key: s.key })),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
for (const [name, value] of Object.entries(values)) {
|
|
46
|
+
await backend.setSecret(opts.site, name, value);
|
|
47
|
+
upsertManifestEntry(opts.site, {
|
|
48
|
+
name,
|
|
49
|
+
kind: 'opaque',
|
|
50
|
+
description: `Extracted via ${matched.join('+') || 'login'}`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
backend: backend.id,
|
|
56
|
+
cookieCount: cookies.length,
|
|
57
|
+
storageCount: storage.length,
|
|
58
|
+
values,
|
|
59
|
+
matchedExtractors: matched,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** End snapshot captures everything set during the workflow (post-login
|
|
64
|
+
* cookies); fall back to start snapshot if absent. */
|
|
65
|
+
function collectCookies(session: Session) {
|
|
66
|
+
const snaps = session.cookieSnapshots ?? [];
|
|
67
|
+
const end = snaps.find((s) => s.label === 'end');
|
|
68
|
+
const start = snaps.find((s) => s.label === 'start');
|
|
69
|
+
const chosen = end ?? start;
|
|
70
|
+
if (!chosen) return [];
|
|
71
|
+
return chosen.cookies.map((c) => ({ ...c }));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function collectStorage(session: Session): StorageRecord[] {
|
|
75
|
+
const snaps = session.storageSnapshots ?? [];
|
|
76
|
+
const end = snaps.filter((s) => s.label === 'end');
|
|
77
|
+
const chosen = end.length > 0 ? end : snaps.filter((s) => s.label === 'start');
|
|
78
|
+
const byKey = new Map<string, StorageRecord>();
|
|
79
|
+
for (const snap of chosen) {
|
|
80
|
+
for (const [key, value] of Object.entries(snap.localStorage ?? {})) {
|
|
81
|
+
byKey.set(`${snap.origin}\0localStorage\0${key}`, {
|
|
82
|
+
origin: snap.origin,
|
|
83
|
+
kind: 'localStorage',
|
|
84
|
+
key,
|
|
85
|
+
value,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return Array.from(byKey.values());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Per-site extractors pull named values out of recognized auth shapes;
|
|
93
|
+
* ordered list, first match wins. */
|
|
94
|
+
const EXTRACTORS: Array<{
|
|
95
|
+
name: string;
|
|
96
|
+
match: (session: Session) => Record<string, string> | null;
|
|
97
|
+
}> = [
|
|
98
|
+
{
|
|
99
|
+
name: 'discoverandgo:Login',
|
|
100
|
+
// D&G's Login POST returns a JSON object with patronID.
|
|
101
|
+
match: (session) => {
|
|
102
|
+
const loginReq = session.requests.find(
|
|
103
|
+
(r) =>
|
|
104
|
+
r.method === 'POST' &&
|
|
105
|
+
r.url.includes('epass_server.php') &&
|
|
106
|
+
(r.body?.includes('method=Login') ?? false),
|
|
107
|
+
);
|
|
108
|
+
if (!loginReq?.response?.body) return null;
|
|
109
|
+
try {
|
|
110
|
+
const body = JSON.parse(loginReq.response.body) as {
|
|
111
|
+
patronID?: string;
|
|
112
|
+
session?: string;
|
|
113
|
+
patronEmail?: string;
|
|
114
|
+
};
|
|
115
|
+
const out: Record<string, string> = {};
|
|
116
|
+
if (body.patronID) out.patron_id = body.patronID;
|
|
117
|
+
if (body.session) out.session_id = body.session;
|
|
118
|
+
if (body.patronEmail) out.patron_email = body.patronEmail;
|
|
119
|
+
return Object.keys(out).length ? out : null;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'southwest:security_token',
|
|
127
|
+
// Southwest's POST /api/security/v4/security/token returns auth tokens
|
|
128
|
+
// and account info we want available to follow-up requests.
|
|
129
|
+
match: (session) => {
|
|
130
|
+
const loginReq = session.requests.find(
|
|
131
|
+
(r) =>
|
|
132
|
+
r.method === 'POST' &&
|
|
133
|
+
r.url.includes('/api/security/v4/security/token') &&
|
|
134
|
+
(r.body?.includes('username=') ?? false),
|
|
135
|
+
);
|
|
136
|
+
if (!loginReq?.response?.body) return null;
|
|
137
|
+
try {
|
|
138
|
+
const body = JSON.parse(loginReq.response.body) as Record<string, unknown>;
|
|
139
|
+
const out: Record<string, string> = {};
|
|
140
|
+
const accountNumber = body['customers.userInformation.accountNumber'];
|
|
141
|
+
const primaryEmail = body['customers.userInformation.primaryEmail'];
|
|
142
|
+
if (typeof accountNumber === 'string') out.account_number = accountNumber;
|
|
143
|
+
if (typeof primaryEmail === 'string') out.primary_email = primaryEmail;
|
|
144
|
+
return Object.keys(out).length ? out : null;
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
function extractKnownValues(session: Session): {
|
|
153
|
+
values: Record<string, string>;
|
|
154
|
+
matched: string[];
|
|
155
|
+
} {
|
|
156
|
+
const values: Record<string, string> = {};
|
|
157
|
+
const matched: string[] = [];
|
|
158
|
+
for (const ext of EXTRACTORS) {
|
|
159
|
+
const v = ext.match(session);
|
|
160
|
+
if (v) {
|
|
161
|
+
Object.assign(values, v);
|
|
162
|
+
matched.push(ext.name);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { values, matched };
|
|
166
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio MCP server that exposes the compile-agent's tools to claude-cli.
|
|
3
|
+
*
|
|
4
|
+
* Spawned by `claude-cli-compile.ts` via `--mcp-config`. The server registers
|
|
5
|
+
* the same 8 read/write tools the in-process loop uses, plus a custom `done`
|
|
6
|
+
* tool that runs external verification inline and writes a sentinel file when
|
|
7
|
+
* complete. claude-cli polls the sentinel and SIGTERMs us when it appears.
|
|
8
|
+
*
|
|
9
|
+
* Why in-tool verification: the in-process loop (agent.ts) restarts after a
|
|
10
|
+
* verification failure with a continuation message. Doing the same here would
|
|
11
|
+
* require killing claude-cli and re-spawning, losing context. Instead, we
|
|
12
|
+
* return the failure list as the tool_result content so claude continues
|
|
13
|
+
* iterating in the same conversation — same up-to-5-cycle bound, no context
|
|
14
|
+
* loss.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { writeFileSync } from 'node:fs';
|
|
18
|
+
import { join as pathJoin } from 'node:path';
|
|
19
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
20
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
21
|
+
import {
|
|
22
|
+
CallToolRequestSchema,
|
|
23
|
+
type CallToolResult,
|
|
24
|
+
ListToolsRequestSchema,
|
|
25
|
+
type Tool,
|
|
26
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
27
|
+
import { buildCompileTools, externalVerification } from './compile-tools.ts';
|
|
28
|
+
import { loadJsonFile } from './load-json.ts';
|
|
29
|
+
import { createLog } from './log.ts';
|
|
30
|
+
import { redactSession } from './redact.ts';
|
|
31
|
+
import type { SharedCompileContext, ToolCandidate } from './tool-candidates.ts';
|
|
32
|
+
import { type Session, SessionSchema } from './types.ts';
|
|
33
|
+
|
|
34
|
+
const log = createLog('mcp-compile');
|
|
35
|
+
|
|
36
|
+
interface RunCompileMcpServerOptions {
|
|
37
|
+
/** Path to the recorded session JSON. */
|
|
38
|
+
sessionPath: string;
|
|
39
|
+
/** Absolute path to the generated tool directory where artifacts go. */
|
|
40
|
+
toolDir: string;
|
|
41
|
+
/** Hard cap on done() verification failures before we permanently fail.
|
|
42
|
+
* Mirrors compile-agent.ts MAX_VERIFICATION_CYCLES. */
|
|
43
|
+
maxVerificationCycles?: number;
|
|
44
|
+
candidate?: ToolCandidate;
|
|
45
|
+
sharedContext?: SharedCompileContext;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DONE_SENTINEL = '.compile-done.json';
|
|
49
|
+
const GIVE_UP_SENTINEL = '.compile-give-up.json';
|
|
50
|
+
|
|
51
|
+
export async function runCompileMcpServer(opts: RunCompileMcpServerOptions): Promise<void> {
|
|
52
|
+
const maxVerificationCycles = opts.maxVerificationCycles ?? 5;
|
|
53
|
+
|
|
54
|
+
// Load + auto-redact the session, exactly as compile-agent.ts does.
|
|
55
|
+
let session: Session = loadJsonFile(
|
|
56
|
+
opts.sessionPath,
|
|
57
|
+
SessionSchema,
|
|
58
|
+
{
|
|
59
|
+
notFound: '→ run `imprint record <site>` to create one.',
|
|
60
|
+
notJson: `→ if it's a partial .jsonl, run \`imprint assemble ${opts.sessionPath}\` first.`,
|
|
61
|
+
badSchema: '→ check the file came from `imprint record`.',
|
|
62
|
+
},
|
|
63
|
+
'session',
|
|
64
|
+
);
|
|
65
|
+
const looksRedacted = JSON.stringify(session).includes('[REDACTED:');
|
|
66
|
+
if (!looksRedacted) {
|
|
67
|
+
session = redactSession(session).session;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Build the 8 read/write tools (same as the in-process loop).
|
|
71
|
+
const compileTools = buildCompileTools(session, opts.toolDir, opts.sessionPath, {
|
|
72
|
+
candidate: opts.candidate,
|
|
73
|
+
sharedContext: opts.sharedContext,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// The custom done/give_up tools live alongside in MCP space.
|
|
77
|
+
const doneTool: Tool = {
|
|
78
|
+
name: 'done',
|
|
79
|
+
description:
|
|
80
|
+
'Call this when you have successfully completed the task. Triggers external verification of the artifacts. If verification fails, the result will list the issues and you should fix them and call done again.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
summary: { type: 'string', description: 'Brief summary of what was accomplished' },
|
|
85
|
+
},
|
|
86
|
+
required: ['summary'],
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
const giveUpTool: Tool = {
|
|
90
|
+
name: 'give_up',
|
|
91
|
+
description:
|
|
92
|
+
'Call this when you have encountered a categorical impossibility and cannot proceed.',
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
reason: { type: 'string', description: 'Why you cannot complete the task' },
|
|
97
|
+
what_was_tried: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description: 'Summary of approaches you tried before giving up',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
required: ['reason', 'what_was_tried'],
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
let verificationFailures = 0;
|
|
107
|
+
|
|
108
|
+
const server = new Server(
|
|
109
|
+
{ name: 'imprint-compile', version: '0.1.0' },
|
|
110
|
+
{
|
|
111
|
+
capabilities: { tools: {} },
|
|
112
|
+
instructions:
|
|
113
|
+
'These tools let you reverse-engineer the captured session into workflow.json + parser.ts + parser.test.ts. Read the recording, write the artifacts, run tests, and call done() when verified. The done tool runs external verification and will tell you what to fix if anything is wrong.',
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
118
|
+
tools: [
|
|
119
|
+
...compileTools.map(
|
|
120
|
+
(t): Tool => ({
|
|
121
|
+
name: t.name,
|
|
122
|
+
description: t.description,
|
|
123
|
+
inputSchema: t.input_schema as Tool['inputSchema'],
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
doneTool,
|
|
127
|
+
giveUpTool,
|
|
128
|
+
],
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
server.setRequestHandler(CallToolRequestSchema, async (req): Promise<CallToolResult> => {
|
|
132
|
+
const name = req.params.name;
|
|
133
|
+
const args = req.params.arguments ?? {};
|
|
134
|
+
|
|
135
|
+
// Custom done — runs verification inline.
|
|
136
|
+
if (name === 'done') {
|
|
137
|
+
const summary = (args as { summary?: string }).summary ?? 'Task completed';
|
|
138
|
+
log(`done() called: ${summary}`);
|
|
139
|
+
const { failures, warnings } = await externalVerification(
|
|
140
|
+
opts.toolDir,
|
|
141
|
+
session,
|
|
142
|
+
opts.sessionPath,
|
|
143
|
+
{
|
|
144
|
+
expectedToolName: opts.candidate?.toolName,
|
|
145
|
+
likelyParams: opts.candidate?.likelyParams,
|
|
146
|
+
candidateRequestSeqs: opts.candidate?.requestSeqs,
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
if (warnings.length > 0) {
|
|
150
|
+
log(`verification warnings (non-blocking):\n${warnings.join('\n')}`);
|
|
151
|
+
}
|
|
152
|
+
if (failures.length === 0) {
|
|
153
|
+
const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
|
|
154
|
+
writeFileSync(
|
|
155
|
+
sentinel,
|
|
156
|
+
JSON.stringify(
|
|
157
|
+
{ summary, verification: 'passed', warnings, timestamp: Date.now() },
|
|
158
|
+
null,
|
|
159
|
+
2,
|
|
160
|
+
),
|
|
161
|
+
'utf8',
|
|
162
|
+
);
|
|
163
|
+
log(`verification passed; wrote ${sentinel}`);
|
|
164
|
+
return {
|
|
165
|
+
content: [
|
|
166
|
+
{
|
|
167
|
+
type: 'text',
|
|
168
|
+
text: 'DONE_VERIFIED — verification passed. The orchestrator will exit shortly. Do not call any more tools.',
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
verificationFailures++;
|
|
175
|
+
log(`verification failed (cycle ${verificationFailures}/${maxVerificationCycles})`);
|
|
176
|
+
if (verificationFailures >= maxVerificationCycles) {
|
|
177
|
+
const sentinel = pathJoin(opts.toolDir, DONE_SENTINEL);
|
|
178
|
+
writeFileSync(
|
|
179
|
+
sentinel,
|
|
180
|
+
JSON.stringify(
|
|
181
|
+
{
|
|
182
|
+
summary,
|
|
183
|
+
verification: 'failed',
|
|
184
|
+
cycles: verificationFailures,
|
|
185
|
+
failures,
|
|
186
|
+
warnings,
|
|
187
|
+
timestamp: Date.now(),
|
|
188
|
+
},
|
|
189
|
+
null,
|
|
190
|
+
2,
|
|
191
|
+
),
|
|
192
|
+
'utf8',
|
|
193
|
+
);
|
|
194
|
+
return {
|
|
195
|
+
isError: true,
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: 'text',
|
|
199
|
+
text: `Verification failed after ${maxVerificationCycles} cycles. Giving up. Final failures:\n${failures.map((f) => `- ${f}`).join('\n')}`,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const continuationMessage = `You called done but verification failed (cycle ${verificationFailures}/${maxVerificationCycles}):
|
|
206
|
+
|
|
207
|
+
${failures.map((f) => `- ${f}`).join('\n')}
|
|
208
|
+
|
|
209
|
+
Resume your work. Read the files you wrote (workflow.json, parser.ts, parser.test.ts), fix the issues, re-run tests, and call done again when fixed.`;
|
|
210
|
+
return {
|
|
211
|
+
isError: true,
|
|
212
|
+
content: [{ type: 'text', text: continuationMessage }],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Custom give_up — writes sentinel and exits.
|
|
217
|
+
if (name === 'give_up') {
|
|
218
|
+
const reason = (args as { reason?: string }).reason ?? 'unknown';
|
|
219
|
+
const whatWasTried = (args as { what_was_tried?: string }).what_was_tried ?? '';
|
|
220
|
+
log(`give_up() called: ${reason}`);
|
|
221
|
+
const sentinel = pathJoin(opts.toolDir, GIVE_UP_SENTINEL);
|
|
222
|
+
writeFileSync(
|
|
223
|
+
sentinel,
|
|
224
|
+
JSON.stringify({ reason, what_was_tried: whatWasTried, timestamp: Date.now() }, null, 2),
|
|
225
|
+
'utf8',
|
|
226
|
+
);
|
|
227
|
+
return {
|
|
228
|
+
content: [
|
|
229
|
+
{
|
|
230
|
+
type: 'text',
|
|
231
|
+
text: 'GIVE_UP_RECORDED — the orchestrator will exit shortly. Do not call any more tools.',
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Standard read/write tools — delegate to the shared handlers.
|
|
238
|
+
const tool = compileTools.find((t) => t.name === name);
|
|
239
|
+
if (!tool) {
|
|
240
|
+
return {
|
|
241
|
+
isError: true,
|
|
242
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let result: { result: string; isError?: boolean };
|
|
247
|
+
try {
|
|
248
|
+
result = await tool.handler(args);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
result = {
|
|
251
|
+
result: err instanceof Error ? err.message : String(err),
|
|
252
|
+
isError: true,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
content: [{ type: 'text', text: result.result }],
|
|
258
|
+
isError: result.isError ?? false,
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const transport = new StdioServerTransport();
|
|
263
|
+
await server.connect(transport);
|
|
264
|
+
log(`stdio transport ready (${compileTools.length + 2} tools)`);
|
|
265
|
+
|
|
266
|
+
// Block until the orchestrator closes us. Mirrors mcp-server.ts:230.
|
|
267
|
+
await new Promise<void>((resolve) => {
|
|
268
|
+
const close = (reason: string): void => {
|
|
269
|
+
log(`closing: ${reason}`);
|
|
270
|
+
resolve();
|
|
271
|
+
};
|
|
272
|
+
transport.onclose = () => close('client disconnected');
|
|
273
|
+
process.once('SIGINT', () => close('SIGINT'));
|
|
274
|
+
process.once('SIGTERM', () => close('SIGTERM'));
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Sentinel file names exposed for the orchestrator to poll. */
|
|
279
|
+
export const COMPILE_SENTINELS = {
|
|
280
|
+
done: DONE_SENTINEL,
|
|
281
|
+
giveUp: GIVE_UP_SENTINEL,
|
|
282
|
+
} as const;
|