@quantod/qq 0.3.6 → 1.0.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/commands.d.ts +23 -7
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +134 -30
- package/dist/commands.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +11 -17
- package/dist/db.js.map +1 -1
- package/dist/http.d.ts +7 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +113 -0
- package/dist/http.js.map +1 -0
- package/dist/index.js +54 -30
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +232 -0
- package/dist/mcp.js.map +1 -0
- package/dist/resources/chrome.md +33 -0
- package/dist/resources/guide.md +491 -0
- package/dist/resources/pipeline_design.md +192 -0
- package/dist/sdk-templates/javascript.d.ts +2 -0
- package/dist/sdk-templates/javascript.d.ts.map +1 -0
- package/dist/sdk-templates/javascript.js +24 -0
- package/dist/sdk-templates/javascript.js.map +1 -0
- package/dist/sdk-templates/python.d.ts +2 -0
- package/dist/sdk-templates/python.d.ts.map +1 -0
- package/dist/sdk-templates/python.js +31 -0
- package/dist/sdk-templates/python.js.map +1 -0
- package/package.json +11 -4
- package/src/commands.ts +151 -32
- package/src/db.ts +12 -18
- package/src/http.ts +133 -0
- package/src/index.ts +54 -33
- package/src/mcp.ts +226 -0
- package/src/resources/chrome.md +33 -0
- package/src/resources/guide.md +491 -0
- package/src/resources/pipeline_design.md +192 -0
- package/src/sdk-templates/javascript.ts +20 -0
- package/src/sdk-templates/python.ts +27 -0
package/src/index.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { Command, InvalidArgumentError } from 'commander';
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
2
|
import { readFileSync } from 'node:fs';
|
|
4
3
|
import { createInterface } from 'node:readline';
|
|
5
|
-
import {
|
|
4
|
+
import { join } from 'node:path';
|
|
6
5
|
import { dump } from 'js-yaml';
|
|
7
6
|
import {
|
|
8
|
-
makePipeline, deletePipeline, push, claim, release,
|
|
9
|
-
batchRead,
|
|
10
|
-
type FilterOptions, type PushOptions, type ClaimOptions,
|
|
7
|
+
makePipeline, listPipelines, deletePipeline, push, claim, release,
|
|
8
|
+
batchRead, status, unstick, loadFile, QQError,
|
|
9
|
+
type FilterOptions, type PushOptions, type ClaimOptions, type PayloadFormat,
|
|
11
10
|
} from './commands.js';
|
|
12
11
|
|
|
13
12
|
function out(data: unknown): void {
|
|
@@ -49,7 +48,7 @@ function parsePath(arg: string): { pipeline: string; stage?: string } {
|
|
|
49
48
|
|
|
50
49
|
function collectFilters(opts: Record<string, unknown>): FilterOptions {
|
|
51
50
|
const f: FilterOptions = {};
|
|
52
|
-
if (opts['claimed'] !== undefined) f.claimed = opts['claimed'] === '
|
|
51
|
+
if (opts['claimed'] !== undefined) f.claimed = opts['claimed'] === 'true';
|
|
53
52
|
if (opts['ids']) f.ids = (opts['ids'] as string).split(',');
|
|
54
53
|
if (opts['createdAfter']) f.created_after = opts['createdAfter'] as number;
|
|
55
54
|
if (opts['createdBefore']) f.created_before = opts['createdBefore'] as number;
|
|
@@ -61,7 +60,7 @@ function collectFilters(opts: Record<string, unknown>): FilterOptions {
|
|
|
61
60
|
|
|
62
61
|
function addFilterOptions(cmd: Command): Command {
|
|
63
62
|
return cmd
|
|
64
|
-
.option('--claimed <
|
|
63
|
+
.option('--claimed <true|false>')
|
|
65
64
|
.option('--ids <id1,id2,...>')
|
|
66
65
|
.option('--created-after <epoch>', '', parseIntArg)
|
|
67
66
|
.option('--created-before <epoch>', '', parseIntArg)
|
|
@@ -78,16 +77,32 @@ function addReapFilterOptions(cmd: Command): Command {
|
|
|
78
77
|
.option('--modified-after <epoch>', '', parseIntArg);
|
|
79
78
|
}
|
|
80
79
|
|
|
80
|
+
const { version } = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8')) as { version: string };
|
|
81
|
+
|
|
81
82
|
const program = new Command();
|
|
82
|
-
program.name('qq').description('Persistent pipeline for Claude agent workflows');
|
|
83
|
+
program.name('qq').description('Persistent pipeline for Claude agent workflows').version(version);
|
|
83
84
|
|
|
84
85
|
program
|
|
85
86
|
.command('make-pipeline [name]')
|
|
86
|
-
.
|
|
87
|
-
|
|
87
|
+
.option('--description <text>', 'human-readable description of the pipeline')
|
|
88
|
+
.action((name, opts) => {
|
|
89
|
+
try { out({ pipeline: makePipeline(name, opts.description) }); }
|
|
88
90
|
catch (e) { fail(e instanceof Error ? e.message : String(e)); }
|
|
89
91
|
});
|
|
90
92
|
|
|
93
|
+
program
|
|
94
|
+
.command('list-pipelines')
|
|
95
|
+
.action(() => {
|
|
96
|
+
try {
|
|
97
|
+
const names = listPipelines();
|
|
98
|
+
if (names.length === 0) {
|
|
99
|
+
process.stdout.write('(no pipelines)\n');
|
|
100
|
+
} else {
|
|
101
|
+
process.stdout.write(names.join('\n') + '\n');
|
|
102
|
+
}
|
|
103
|
+
} catch (e) { fail(e instanceof Error ? e.message : String(e)); }
|
|
104
|
+
});
|
|
105
|
+
|
|
91
106
|
program
|
|
92
107
|
.command('delete-pipeline <pipeline>')
|
|
93
108
|
.action((pipeline) => {
|
|
@@ -108,12 +123,11 @@ program
|
|
|
108
123
|
program
|
|
109
124
|
.command('push <path> [id]')
|
|
110
125
|
.option('--priority <n>', 'item priority (higher = claimed first)', parseFloatArg, 0.0)
|
|
111
|
-
.option('--
|
|
126
|
+
.option('--payload-format <yaml|json|text>', 'payload validation: yaml (default), json (minified), text (none)')
|
|
112
127
|
.action((path, id, opts) => {
|
|
113
128
|
try {
|
|
114
|
-
const { pipeline, stage } = parsePath(path);
|
|
115
|
-
|
|
116
|
-
const o: PushOptions = { payload: readStdin() ?? undefined, priority: opts.priority, plainText: opts.plainText ?? false };
|
|
129
|
+
const { pipeline, stage = '' } = parsePath(path);
|
|
130
|
+
const o: PushOptions = { payload: readStdin() ?? undefined, priority: opts.priority, payloadFormat: opts.payloadFormat as PayloadFormat | undefined };
|
|
117
131
|
out({ id: push(pipeline, stage, id, o) });
|
|
118
132
|
} catch (e) { e instanceof QQError ? fail(e.message) : fail(String(e)); }
|
|
119
133
|
});
|
|
@@ -140,36 +154,34 @@ program
|
|
|
140
154
|
.option('--target <stage>', 'move item to this stage (default: stay in current stage)')
|
|
141
155
|
.option('--replace')
|
|
142
156
|
.option('--priority <n>', 'update item priority', parseFloatArg)
|
|
143
|
-
.option('--
|
|
157
|
+
.option('--payload-format <yaml|json|text>', 'payload validation: yaml (default), json (minified), text (none)')
|
|
144
158
|
.action((pipeline, seqStr, opts) => {
|
|
145
159
|
try {
|
|
146
160
|
const seq = parseInt(seqStr, 10);
|
|
147
161
|
if (isNaN(seq)) fail('seq must be an integer');
|
|
148
162
|
const payload = readStdin() ?? undefined;
|
|
149
|
-
release(pipeline, seq, { target: opts.target, payload, replace: opts.replace, priority: opts.priority,
|
|
163
|
+
release(pipeline, seq, { target: opts.target, payload, replace: opts.replace, priority: opts.priority, payloadFormat: opts.payloadFormat as PayloadFormat | undefined });
|
|
150
164
|
out({ ok: true });
|
|
151
165
|
} catch (e) { e instanceof QQError ? fail(e.message) : fail(String(e)); }
|
|
152
166
|
});
|
|
153
167
|
|
|
154
168
|
addFilterOptions(program.command('batch-read <path>'))
|
|
155
|
-
.option('--payload', 'include payload in output')
|
|
169
|
+
.option('--include-payload', 'include payload in output')
|
|
156
170
|
.action((path, opts) => {
|
|
157
171
|
try {
|
|
158
172
|
const { pipeline, stage = '*' } = parsePath(path);
|
|
159
173
|
const filters = collectFilters(opts);
|
|
160
174
|
if (filters.claimed === undefined) filters.claimed = false;
|
|
161
|
-
out(batchRead(pipeline, stage, filters, opts.
|
|
175
|
+
out(batchRead(pipeline, stage, filters, opts.includePayload ?? false));
|
|
162
176
|
}
|
|
163
177
|
catch (e) { fail(e instanceof Error ? e.message : String(e)); }
|
|
164
178
|
});
|
|
165
179
|
|
|
166
180
|
program
|
|
167
|
-
.command('
|
|
181
|
+
.command('status <pipeline>')
|
|
168
182
|
.action((pipeline) => {
|
|
169
183
|
try {
|
|
170
|
-
|
|
171
|
-
process.stdout.write('# pipeline[/stage][/substage][...]: [total, claimed]\n');
|
|
172
|
-
process.stdout.write(dump(data, { lineWidth: -1, flowLevel: 1 }));
|
|
184
|
+
out(status(pipeline));
|
|
173
185
|
}
|
|
174
186
|
catch (e) { fail(e instanceof Error ? e.message : String(e)); }
|
|
175
187
|
});
|
|
@@ -184,17 +196,26 @@ addReapFilterOptions(program.command('unstick <path>'))
|
|
|
184
196
|
});
|
|
185
197
|
|
|
186
198
|
program
|
|
187
|
-
.command('
|
|
188
|
-
.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
199
|
+
.command('load-file <path> <pipeline>')
|
|
200
|
+
.description('Load multiple items from a file into a pipeline (JSONL, JSON array, YAML array, CSV)')
|
|
201
|
+
.option('--stage <stage>', 'default stage for records that do not specify one')
|
|
202
|
+
.option('--delete-after', 'delete the file after loading')
|
|
203
|
+
.action((filePath, pipeline, opts) => {
|
|
204
|
+
try {
|
|
205
|
+
const results = loadFile(filePath, pipeline, { stage: opts.stage, deleteAfter: opts.deleteAfter ?? false });
|
|
206
|
+
for (const r of results) {
|
|
207
|
+
process.stdout.write(r.message ? `${r.id}: ${r.status}: ${r.message}\n` : `${r.id}: ${r.status}\n`);
|
|
208
|
+
}
|
|
209
|
+
} catch (e) { fail(e instanceof Error ? e.message : String(e)); }
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
program
|
|
213
|
+
.command('mcp')
|
|
214
|
+
.description('Start MCP server (stdio) with embedded HTTP bridge')
|
|
215
|
+
.option('--port <n>', 'HTTP bridge port (default: random)', (v) => parseInt(v, 10))
|
|
216
|
+
.action(async (opts) => {
|
|
217
|
+
const { startMcp } = await import('./mcp.js');
|
|
218
|
+
await startMcp(opts.port);
|
|
198
219
|
});
|
|
199
220
|
|
|
200
221
|
program.parse();
|
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { networkInterfaces } from 'node:os';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { dump } from 'js-yaml';
|
|
8
|
+
import {
|
|
9
|
+
makePipeline, deletePipeline, push, claim, release,
|
|
10
|
+
batchRead, status, unstick, loadFile,
|
|
11
|
+
} from './commands.js';
|
|
12
|
+
import { startHttp } from './http.js';
|
|
13
|
+
import { renderJavaScriptSdk } from './sdk-templates/javascript.js';
|
|
14
|
+
import { renderPythonSdk } from './sdk-templates/python.js';
|
|
15
|
+
|
|
16
|
+
const VERSION = JSON.parse(
|
|
17
|
+
readFileSync(join(__dirname, '../package.json'), 'utf8')
|
|
18
|
+
).version as string;
|
|
19
|
+
|
|
20
|
+
function loadResource(name: string): string {
|
|
21
|
+
return readFileSync(join(__dirname, 'resources', `${name}.md`), 'utf8');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const INSTRUCTIONS = `QQ is a persistent, crash-safe pipeline for multi-stage agentic workflows. \
|
|
25
|
+
It implements a state machine with concurrency guarantees: items move through stages, \
|
|
26
|
+
claim→release is an atomic transition only one agent holds at a time, and state survives restarts.
|
|
27
|
+
|
|
28
|
+
Use QQ when work fans out across multiple items or stages: bulk processing, web crawling, \
|
|
29
|
+
research pipelines, batch enrichment, any task where multiple items need to be processed \
|
|
30
|
+
concurrently or sequentially through defined stages.
|
|
31
|
+
|
|
32
|
+
Before calling any QQ tool, read the \`guide\` resource — it contains the mental model, \
|
|
33
|
+
the complete tool reference, SDK usage, and examples. For Chrome browser scenarios, \
|
|
34
|
+
also read the \`chrome\` resource. When designing a new pipeline, read the \`pipeline_design\` resource first.`;
|
|
35
|
+
|
|
36
|
+
function getLanIp(): string {
|
|
37
|
+
const ifaces = networkInterfaces();
|
|
38
|
+
for (const name of Object.keys(ifaces)) {
|
|
39
|
+
if (name.startsWith('lo')) continue;
|
|
40
|
+
for (const iface of ifaces[name] ?? []) {
|
|
41
|
+
if (iface.family === 'IPv4' && !iface.internal) return iface.address;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return '127.0.0.1';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ok(text: string) {
|
|
48
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function err(e: unknown) {
|
|
52
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
53
|
+
return { content: [{ type: 'text' as const, text: `error: ${msg}` }], isError: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function startMcp(fixedPort?: number): Promise<void> {
|
|
57
|
+
const http = await startHttp(fixedPort);
|
|
58
|
+
const lanIp = getLanIp();
|
|
59
|
+
|
|
60
|
+
const server = new McpServer(
|
|
61
|
+
{ name: 'qq', version: VERSION },
|
|
62
|
+
{ instructions: INSTRUCTIONS },
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// ── Pipeline tools ─────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
server.registerTool('make_pipeline', {
|
|
68
|
+
description: 'Create a new pipeline. Read guide before using.',
|
|
69
|
+
inputSchema: { name: z.string().optional(), description: z.string().optional() },
|
|
70
|
+
}, ({ name, description }) => {
|
|
71
|
+
try { return ok(`pipeline: ${makePipeline(name, description)}`); }
|
|
72
|
+
catch (e) { return err(e); }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
server.registerTool('push', {
|
|
76
|
+
description: 'Push an item into a pipeline stage. Read guide before using.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
pipeline: z.string(),
|
|
79
|
+
stage: z.string().optional(),
|
|
80
|
+
id: z.string().optional(),
|
|
81
|
+
payload: z.string().optional(),
|
|
82
|
+
priority: z.number().optional(),
|
|
83
|
+
payloadFormat: z.enum(['yaml', 'json', 'text']).optional(),
|
|
84
|
+
},
|
|
85
|
+
}, ({ pipeline, stage, id, payload, priority, payloadFormat }) => {
|
|
86
|
+
try { return ok(`id: ${push(pipeline, stage, id, { payload, priority, payloadFormat })}`); }
|
|
87
|
+
catch (e) { return err(e); }
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
server.registerTool('claim', {
|
|
91
|
+
description: 'Claim the next available item from a pipeline stage. Read guide before using.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
pipeline: z.string(),
|
|
94
|
+
stage: z.string().default('*'),
|
|
95
|
+
id: z.string().optional(),
|
|
96
|
+
random: z.boolean().optional(),
|
|
97
|
+
},
|
|
98
|
+
}, ({ pipeline, stage, id, random }) => {
|
|
99
|
+
try { return ok(dump(claim(pipeline, stage, id, { random }), { lineWidth: -1 })); }
|
|
100
|
+
catch (e) { return err(e); }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
server.registerTool('release', {
|
|
104
|
+
description: 'Release a claimed item, optionally moving it to a new stage. Read guide before using.',
|
|
105
|
+
inputSchema: {
|
|
106
|
+
pipeline: z.string(),
|
|
107
|
+
seq: z.number(),
|
|
108
|
+
target: z.string().optional(),
|
|
109
|
+
payload: z.string().optional(),
|
|
110
|
+
replace: z.boolean().optional(),
|
|
111
|
+
priority: z.number().optional(),
|
|
112
|
+
payloadFormat: z.enum(['yaml', 'json', 'text']).optional(),
|
|
113
|
+
},
|
|
114
|
+
}, ({ pipeline, seq, target, payload, replace, priority, payloadFormat }) => {
|
|
115
|
+
try { release(pipeline, seq, { target, payload, replace, priority, payloadFormat }); return ok('ok: true'); }
|
|
116
|
+
catch (e) { return err(e); }
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
server.registerTool('status', {
|
|
120
|
+
description: 'Get pipeline description and item counts per stage. Read guide before using.',
|
|
121
|
+
inputSchema: { pipeline: z.string() },
|
|
122
|
+
}, ({ pipeline }) => {
|
|
123
|
+
try { return ok(dump(status(pipeline), { lineWidth: -1 })); }
|
|
124
|
+
catch (e) { return err(e); }
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
server.registerTool('batch_read', {
|
|
128
|
+
description: 'Read items from a pipeline stage without claiming. Read guide before using.',
|
|
129
|
+
inputSchema: {
|
|
130
|
+
pipeline: z.string(),
|
|
131
|
+
stage: z.string().optional(),
|
|
132
|
+
includePayload: z.boolean().optional(),
|
|
133
|
+
claimed: z.boolean().optional(),
|
|
134
|
+
ids: z.array(z.string()).optional(),
|
|
135
|
+
createdAfter: z.number().optional(),
|
|
136
|
+
createdBefore: z.number().optional(),
|
|
137
|
+
modifiedAfter: z.number().optional(),
|
|
138
|
+
limit: z.number().optional(),
|
|
139
|
+
offset: z.number().optional(),
|
|
140
|
+
},
|
|
141
|
+
}, ({ pipeline, stage, includePayload, claimed, ids, createdAfter, createdBefore, modifiedAfter, limit, offset }) => {
|
|
142
|
+
try {
|
|
143
|
+
const items = batchRead(pipeline, stage ?? '*', {
|
|
144
|
+
claimed, ids, created_after: createdAfter, created_before: createdBefore,
|
|
145
|
+
modified_after: modifiedAfter, limit, offset,
|
|
146
|
+
}, includePayload ?? false);
|
|
147
|
+
return ok(dump(items, { lineWidth: -1 }));
|
|
148
|
+
} catch (e) { return err(e); }
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
server.registerTool('unstick', {
|
|
152
|
+
description: 'Release all stuck claimed items back to their stage. Read guide before using.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
pipeline: z.string(),
|
|
155
|
+
stage: z.string().optional(),
|
|
156
|
+
ids: z.array(z.string()).optional(),
|
|
157
|
+
createdAfter: z.number().optional(),
|
|
158
|
+
createdBefore: z.number().optional(),
|
|
159
|
+
modifiedAfter: z.number().optional(),
|
|
160
|
+
},
|
|
161
|
+
}, ({ pipeline, stage, ids, createdAfter, createdBefore, modifiedAfter }) => {
|
|
162
|
+
try {
|
|
163
|
+
const n = unstick(pipeline, stage ?? '*', {
|
|
164
|
+
ids, created_after: createdAfter, created_before: createdBefore, modified_after: modifiedAfter,
|
|
165
|
+
});
|
|
166
|
+
return ok(`unstuck: ${n}`);
|
|
167
|
+
} catch (e) { return err(e); }
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
server.registerTool('delete_pipeline', {
|
|
171
|
+
description: 'Delete a pipeline and all its items. Irreversible. Read guide before using.',
|
|
172
|
+
inputSchema: { pipeline: z.string() },
|
|
173
|
+
}, ({ pipeline }) => {
|
|
174
|
+
try { deletePipeline(pipeline); return ok('ok: true'); }
|
|
175
|
+
catch (e) { return err(e); }
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── Bridge tools ───────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
server.registerTool('get_sdk', {
|
|
181
|
+
description: "Get an SDK snippet for connecting scripts to QQ's HTTP bridge. Read guide before using.",
|
|
182
|
+
inputSchema: { language: z.enum(['javascript', 'python']) },
|
|
183
|
+
}, ({ language }) => {
|
|
184
|
+
const snippet = language === 'python'
|
|
185
|
+
? renderPythonSdk(lanIp, http.port, http.token)
|
|
186
|
+
: renderJavaScriptSdk(lanIp, http.port, http.token);
|
|
187
|
+
return ok(snippet);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
server.registerTool('load_file', {
|
|
191
|
+
description: 'Load multiple items from a file into a pipeline. Supports JSONL, JSON array, YAML array, CSV. Read guide before using.',
|
|
192
|
+
inputSchema: {
|
|
193
|
+
path: z.string(),
|
|
194
|
+
pipeline: z.string(),
|
|
195
|
+
stage: z.string().optional(),
|
|
196
|
+
deleteAfter: z.boolean().optional(),
|
|
197
|
+
},
|
|
198
|
+
}, ({ path: filePath, pipeline, stage, deleteAfter }) => {
|
|
199
|
+
try {
|
|
200
|
+
const results = loadFile(filePath, pipeline, { stage, deleteAfter });
|
|
201
|
+
const lines = results.map(r => r.message ? `${r.id}: ${r.status}: ${r.message}` : `${r.id}: ${r.status}`);
|
|
202
|
+
return ok(lines.join('\n'));
|
|
203
|
+
} catch (e) { return err(e); }
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── Resources ──────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
server.registerResource('guide', new ResourceTemplate('qq://guide', { list: undefined }), { mimeType: 'text/markdown' }, async () => ({
|
|
209
|
+
contents: [{ uri: 'qq://guide', mimeType: 'text/markdown', text: loadResource('guide') }],
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
server.registerResource('chrome', new ResourceTemplate('qq://chrome', { list: undefined }), { mimeType: 'text/markdown' }, async () => ({
|
|
213
|
+
contents: [{ uri: 'qq://chrome', mimeType: 'text/markdown', text: loadResource('chrome') }],
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
server.registerResource('pipeline_design', new ResourceTemplate('qq://pipeline_design', { list: undefined }), { mimeType: 'text/markdown' }, async () => ({
|
|
217
|
+
contents: [{ uri: 'qq://pipeline_design', mimeType: 'text/markdown', text: loadResource('pipeline_design') }],
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
// ── Connect ────────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
const transport = new StdioServerTransport();
|
|
223
|
+
await server.connect(transport);
|
|
224
|
+
|
|
225
|
+
process.stderr.write(`qq MCP server running. HTTP bridge on ${lanIp}:${http.port}\n`);
|
|
226
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# QQ Chrome Integration
|
|
2
|
+
|
|
3
|
+
Read the `guide` resource first — this document covers Chrome-specific patterns only.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## How to get data out of Chrome
|
|
8
|
+
|
|
9
|
+
Chrome devtools and connectors usually struggle to get large data packages from web pages in Chrome. Solution: download + load_file. The browser's download API has no mixed content restrictions. Trigger a download from the HTTPS page, then call the `load_file` MCP tool to ingest the file.
|
|
10
|
+
|
|
11
|
+
**In the injected script (HTTPS page):**
|
|
12
|
+
```javascript
|
|
13
|
+
// Serialize data and trigger a download — no network request, no mixed content issue
|
|
14
|
+
const data = JSON.stringify({ items: extractedItems });
|
|
15
|
+
const blob = new Blob([data], { type: 'application/json' });
|
|
16
|
+
const a = document.createElement('a');
|
|
17
|
+
a.href = URL.createObjectURL(blob);
|
|
18
|
+
a.download = 'qq-data.json';
|
|
19
|
+
a.click();
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Then call load_file:**
|
|
23
|
+
```
|
|
24
|
+
load_file(
|
|
25
|
+
path: "$HOME/Downloads/qq-data.json",
|
|
26
|
+
pipeline: "my_pypeline",
|
|
27
|
+
stage: "inbox",
|
|
28
|
+
deleteAfter: true
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Requirement**: Chrome must be set to auto-download to a known directory (no "ask where to save" prompt). Set the `download` attribute on the anchor to control the filename.
|
|
33
|
+
|