common-qa-mcp 0.1.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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/adapters/factory.d.ts +15 -0
- package/dist/adapters/factory.d.ts.map +1 -0
- package/dist/adapters/factory.js +18 -0
- package/dist/adapters/factory.js.map +1 -0
- package/dist/adapters/remoteMcp/adapter.d.ts +92 -0
- package/dist/adapters/remoteMcp/adapter.d.ts.map +1 -0
- package/dist/adapters/remoteMcp/adapter.js +219 -0
- package/dist/adapters/remoteMcp/adapter.js.map +1 -0
- package/dist/adapters/types.d.ts +77 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/adapters/walnut/actionTypes.d.ts +16 -0
- package/dist/adapters/walnut/actionTypes.d.ts.map +1 -0
- package/dist/adapters/walnut/actionTypes.js +141 -0
- package/dist/adapters/walnut/actionTypes.js.map +1 -0
- package/dist/adapters/walnut/mapper.d.ts +34 -0
- package/dist/adapters/walnut/mapper.d.ts.map +1 -0
- package/dist/adapters/walnut/mapper.js +228 -0
- package/dist/adapters/walnut/mapper.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +36 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +81 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +211 -0
- package/dist/config.js.map +1 -0
- package/dist/connection.d.ts +101 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +190 -0
- package/dist/connection.js.map +1 -0
- package/dist/domain/types.d.ts +279 -0
- package/dist/domain/types.d.ts.map +1 -0
- package/dist/domain/types.js +2 -0
- package/dist/domain/types.js.map +1 -0
- package/dist/domain/validate.d.ts +3 -0
- package/dist/domain/validate.d.ts.map +1 -0
- package/dist/domain/validate.js +29 -0
- package/dist/domain/validate.js.map +1 -0
- package/dist/drafts/store.d.ts +62 -0
- package/dist/drafts/store.d.ts.map +1 -0
- package/dist/drafts/store.js +119 -0
- package/dist/drafts/store.js.map +1 -0
- package/dist/httpMain.d.ts +3 -0
- package/dist/httpMain.d.ts.map +1 -0
- package/dist/httpMain.js +22 -0
- package/dist/httpMain.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/initAgents.d.ts +2 -0
- package/dist/initAgents.d.ts.map +1 -0
- package/dist/initAgents.js +178 -0
- package/dist/initAgents.js.map +1 -0
- package/dist/log.d.ts +14 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +14 -0
- package/dist/log.js.map +1 -0
- package/dist/mcpResult.d.ts +20 -0
- package/dist/mcpResult.d.ts.map +1 -0
- package/dist/mcpResult.js +8 -0
- package/dist/mcpResult.js.map +1 -0
- package/dist/mount.d.ts +12 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/mount.js +126 -0
- package/dist/mount.js.map +1 -0
- package/dist/projectStore.d.ts +5 -0
- package/dist/projectStore.d.ts.map +1 -0
- package/dist/projectStore.js +60 -0
- package/dist/projectStore.js.map +1 -0
- package/dist/requestConnection.d.ts +20 -0
- package/dist/requestConnection.d.ts.map +1 -0
- package/dist/requestConnection.js +44 -0
- package/dist/requestConnection.js.map +1 -0
- package/dist/serve.d.ts +10 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +37 -0
- package/dist/serve.js.map +1 -0
- package/dist/server.d.ts +29 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +76 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/connection.tools.d.ts +16 -0
- package/dist/tools/connection.tools.d.ts.map +1 -0
- package/dist/tools/connection.tools.js +106 -0
- package/dist/tools/connection.tools.js.map +1 -0
- package/dist/tools/discovery.tools.d.ts +16 -0
- package/dist/tools/discovery.tools.d.ts.map +1 -0
- package/dist/tools/discovery.tools.js +369 -0
- package/dist/tools/discovery.tools.js.map +1 -0
- package/dist/tools/markdown.d.ts +13 -0
- package/dist/tools/markdown.d.ts.map +1 -0
- package/dist/tools/markdown.js +65 -0
- package/dist/tools/markdown.js.map +1 -0
- package/dist/tools/recording.tools.d.ts +16 -0
- package/dist/tools/recording.tools.d.ts.map +1 -0
- package/dist/tools/recording.tools.js +378 -0
- package/dist/tools/recording.tools.js.map +1 -0
- package/dist/tools/run.tools.d.ts +13 -0
- package/dist/tools/run.tools.d.ts.map +1 -0
- package/dist/tools/run.tools.js +230 -0
- package/dist/tools/run.tools.js.map +1 -0
- package/dist/tools/schemas.d.ts +560 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +112 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/tools/testcase.tools.d.ts +50 -0
- package/dist/tools/testcase.tools.d.ts.map +1 -0
- package/dist/tools/testcase.tools.js +384 -0
- package/dist/tools/testcase.tools.js.map +1 -0
- package/dist/vendor/actionSignatures.d.ts +117 -0
- package/dist/vendor/actionSignatures.d.ts.map +1 -0
- package/dist/vendor/actionSignatures.js +420 -0
- package/dist/vendor/actionSignatures.js.map +1 -0
- package/dist/vendor/logger.d.ts +38 -0
- package/dist/vendor/logger.d.ts.map +1 -0
- package/dist/vendor/logger.js +174 -0
- package/dist/vendor/logger.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
/** Nothing is scoped until a project is chosen, and the project is NOT in the MCP config — so the
|
|
4
|
+
* agent resolves it at runtime and ASKS when unsure, never assuming one. */
|
|
5
|
+
const PROJECT_NOTE = `
|
|
6
|
+
|
|
7
|
+
## Project first — never assume one
|
|
8
|
+
Every test case, run, and recording lives inside a **project**, and the project is NOT preconfigured. Before any project-scoped action (including web/android/... — the recording is saved into the active project, and the AI model is resolved per project):
|
|
9
|
+
- If the user named a project, resolve it with \`list_projects\`, then \`qa_switch_project({ projectId })\`.
|
|
10
|
+
- If NO project is selected (a tool replies "No project selected" / "select a project", or you're starting fresh and the user hasn't said which), call \`list_projects\` and **ASK the user which project** — then \`qa_switch_project\`. Do NOT guess.
|
|
11
|
+
The choice persists for the rest of the session once set.`;
|
|
12
|
+
/** THE one client-facing agent. Its surface is the lifecycle tools + the 4 technology sub-agent tools;
|
|
13
|
+
* the granular browser/recording tools are INTERNAL to those sub-agents on the product MCP. */
|
|
14
|
+
const COMMON_QA_AGENT = {
|
|
15
|
+
slug: 'common-qa-agent',
|
|
16
|
+
description: 'The single QA agent for the connected product (Walnut / SimplifyQA): discover projects and test ' +
|
|
17
|
+
'cases, create test cases by delegating a recording task to a technology sub-agent (web / android / ' +
|
|
18
|
+
'mainframe / db), run them (local or cloud), and triage the result. One agent for the whole ' +
|
|
19
|
+
'lifecycle — the technology and the product are resolved downstream.',
|
|
20
|
+
model: 'sonnet',
|
|
21
|
+
color: 'purple',
|
|
22
|
+
tools: [
|
|
23
|
+
// Lifecycle (product-neutral, flat)
|
|
24
|
+
'common-qa-mcp/list_projects', 'common-qa-mcp/qa_switch_project', 'common-qa-mcp/project_info',
|
|
25
|
+
'common-qa-mcp/tc_list', 'common-qa-mcp/tc_get', 'common-qa-mcp/tc_save', 'common-qa-mcp/tc_delete',
|
|
26
|
+
'common-qa-mcp/tc_run', 'common-qa-mcp/run_status', 'common-qa-mcp/run_stop', 'common-qa-mcp/cloud_runners',
|
|
27
|
+
// User stories → manual test cases → automate. story_list/get pull up stories; tc_draft_create/update
|
|
28
|
+
// author MANUAL test cases (then tc_save); tc_automate promotes a manual test case to automated in place.
|
|
29
|
+
'common-qa-mcp/story_list', 'common-qa-mcp/story_get',
|
|
30
|
+
'common-qa-mcp/tc_draft_create', 'common-qa-mcp/tc_draft_update', 'common-qa-mcp/tc_automate',
|
|
31
|
+
// Test suites — list / read / edit membership / execute (execute reuses run_status + run_stop)
|
|
32
|
+
'common-qa-mcp/suite_list', 'common-qa-mcp/suite_get', 'common-qa-mcp/suite_run',
|
|
33
|
+
'common-qa-mcp/suite_add_testcases', 'common-qa-mcp/suite_remove_testcases',
|
|
34
|
+
// Modify a test case — read numbered steps, then edit (steps / loop / if-else / nested)
|
|
35
|
+
'common-qa-mcp/journal_grep', 'common-qa-mcp/journal_modify',
|
|
36
|
+
// Technology sub-agents — each a SINGLE delegated tool (granular browser/terminal/recording tools
|
|
37
|
+
// live inside them). web and mainframe are NON-BLOCKING: each returns a sessionId, then you poll
|
|
38
|
+
// its own status tool (web_status / mainframe_status) until the recording is done.
|
|
39
|
+
'common-qa-mcp/web', 'common-qa-mcp/web_status',
|
|
40
|
+
'common-qa-mcp/mainframe', 'common-qa-mcp/mainframe_status',
|
|
41
|
+
'common-qa-mcp/android', 'common-qa-mcp/db',
|
|
42
|
+
// Lists the technology sub-agents + which are available (like list_projects, but for technologies)
|
|
43
|
+
'common-qa-mcp/qa_technology_status',
|
|
44
|
+
],
|
|
45
|
+
body: `You are the **Common QA Agent** — the single agent for the whole test-case lifecycle in the connected QA product (Walnut today; SimplifyQA later). You **discover** what exists, **create** test cases by DELEGATING to a technology sub-agent, **run** them (local or cloud), and **triage** results. You do NOT drive a browser yourself — the granular actions live inside the technology sub-agents. Pick the task the user is asking for; ask when genuinely unsure; never fabricate.
|
|
46
|
+
${PROJECT_NOTE}
|
|
47
|
+
|
|
48
|
+
## Discover — read-only
|
|
49
|
+
- \`list_projects\` (id + name + counts), \`project_info\` (the active project), \`tc_list\` (search / filter existing test cases), \`tc_get\` (read one in full — steps + params).
|
|
50
|
+
- \`suite_list\` (the project's test suites) and \`suite_get\` (one suite with its ordered member test cases).
|
|
51
|
+
- \`qa_technology_status\` — lists the technology sub-agents (web / android / mainframe / db) and which are available (web + mainframe today), the way \`list_projects\` lists projects. CHECK IT before promising a recording: mainframe additionally needs the agent to be on Windows with WINDOWS_MCP_ENABLED, which the tool reports.
|
|
52
|
+
- Describe test cases in plain language (no code); flag obvious gaps (e.g. steps missing locators). Report only what the tools return; if nothing matches, say so.
|
|
53
|
+
|
|
54
|
+
## Create — DELEGATE to a technology sub-agent (you have NO browser tools)
|
|
55
|
+
**When the user asks to record or create a test case, ASK TWO questions FIRST — before any recording setup or the reuse question, and never assume either answer:**
|
|
56
|
+
1. **Which technology?** — **web / mainframe / android / db** (confirm what's actually available with \`qa_technology_status\`; web + mainframe today).
|
|
57
|
+
2. **Manual or automated?** — **automated** = RECORD it by delegating to that technology's sub-agent (real browser/terminal, real locators — the rest of this section). **manual** = DRAFT plain-prose steps, no recording — jump to *Stories → manual test cases* (\`tc_draft_create\` with \`category: "manual"\`).
|
|
58
|
+
Proceed only after BOTH answers: automated → continue below; manual → the manual-drafting flow.
|
|
59
|
+
|
|
60
|
+
Hand the whole recording to the sub-agent for the target technology; it opens a real browser, drives the flow with its own AI loop, records with real locators, and asserts. Recording is **NON-BLOCKING and two-step**: \`web\` STARTS it and returns a \`sessionId\`; you POLL \`web_status\` until it finishes.
|
|
61
|
+
- **Ask about REUSE first — up front, like the other setup questions.** Most flows begin by signing in and end by signing out. Before recording, PROACTIVELY ask whether to reuse an existing test case as a nested step instead of re-recording it — typically an existing **Login** to run before and a **Logout** to run after. Don't wait for the user to bring it up; offer it. If they say yes, find the ids with \`tc_list\` (confirm the match when named loosely) and pass them to \`web\` via \`nestStart\` (runs before) / \`nestEnd\` (runs after).
|
|
62
|
+
- **\`web({ task, url?, profile?, nestStart?, nestEnd? })\`** — \`task\` is the COMPLETE goal for the part you are RECORDING: the app URL, the credentials/data, and what to assert, e.g. *"Sign in at <url> as Admin / admin123, then verify the Dashboard heading is visible."* \`nestStart\`/\`nestEnd\` are arrays of EXISTING test-case ids reused as nested steps around the recording, so the produced draft is composed as **[nestStart…] → recorded activity → [nestEnd…]**. **\`web\` returns immediately with \`{ sessionId, status: "recording" }\` and opens a live browser window** — it does NOT block or return a draft. **Call it ONCE; never retry it** (a browser is already recording).
|
|
63
|
+
- **Then POLL \`web_status({ sessionId })\`** every ~15–20 s until \`status\` is \`"done"\` (or \`"error"\`). While it says \`"recording"\`, wait and poll again — do NOT call \`web\` again. On \`"done"\` it returns the assembled draft (\`{ draftId, tc, stepCount }\`), an UNSAVED draft.
|
|
64
|
+
- **When you reuse a Login via \`nestStart\`, record only what happens AFTER sign-in** — don't record the login again (the nested one does it at run time). Reuse a signed-in \`profile\` so the recording browser starts already authenticated and the recorded steps are pure activity. Same idea for a Logout via \`nestEnd\`: don't record the sign-out.
|
|
65
|
+
- **REVIEW BEFORE SAVING — never auto-save. This applies to EVERY technology.** When the poll tool (\`web_status\`, \`mainframe_status\`, …) reports \`"done"\`, show the user the recorded steps in plain language (including the nested Login/Logout wrappers) and ask them to confirm or request changes. A finished recording is an UNSAVED draft and nothing is persisted until they approve. Call **\`tc_save({ draftId })\`** ONLY when the user EXPLICITLY tells you to (e.g. "save it", "save this one", "persist it", "okay save"). Finishing a recording is NOT permission to save, and neither is the user saying the draft "looks good" / "nice" / "that's right" — those are approval of the CONTENT, not an instruction to persist it; if you are unsure whether a reply was a save instruction, ASK. The user may instead save it themselves in the Walnut UI — that's fine; do NOT also \`tc_save\` (it would duplicate). \`tc_save\` persists it as a DRAFT test case and opens it in the real Walnut UI for review / edit / run.
|
|
66
|
+
- **\`mainframe({ task, appPath?, attachOnly?, nestStart?, nestEnd? })\`** — the SAME two-step shape for TN3270 terminals: it launches the emulator, attaches over HLLAPI, drives the screens (reading, sending text/AID keys, capturing each field by label + row/col), then you **poll \`mainframe_status({ sessionId })\`** until \`"done"\`. Pass \`appPath\` (the emulator SESSION FILE) so the emulator gets launched and step 1 records as "Launch \${app_path}" — omit it only to attach to an already-open emulator. The extension identifies the emulator: **\`.ecf\` = Quick3270, \`.ws\` = IBM Personal Communications, \`.rd3x\`/\`.rd5x\` = Micro Focus Reflection**. **If you do not know the path, ASK THE USER for it — do NOT go searching the filesystem**, and do NOT assume an extension (guessing \`.ws\` on a Quick3270 machine finds nothing and wastes the turn). Two constraints worth stating up front: it needs the agent on **Windows with WINDOWS_MCP_ENABLED**; and mainframe recordings **cannot overlap** (one HLLAPI session admits one attachment, so a second call is refused, not queued). Mainframe DOES record assertions — \`mf_verify_screen\` / \`mf_verify_field\` run against the live terminal — so include what to verify in the \`task\`, exactly as you would for web.
|
|
67
|
+
- **\`android({ task })\` / \`db({ task })\`** — same shape for their technologies; declared but not built yet (they reply "not available yet").
|
|
68
|
+
- Write ONE self-contained \`task\`: include the URL, the REAL values (passwords too — sensitive fields are parameterized to \`\${...}\` automatically), and the assertion. If a step needs a human (OTP / CAPTCHA / first login), say so in the task so the sub-agent pauses for it.
|
|
69
|
+
|
|
70
|
+
## Stories → manual test cases → automate
|
|
71
|
+
Pull up user stories, draft MANUAL test cases for them, and later promote those to automated.
|
|
72
|
+
- **Discover stories** — **\`story_list({ search? })\`** finds stories by keyword (key / title / description / acceptance criteria); **\`story_get({ id })\`** reads ONE (id or business key like \`US_12\`) with its **acceptance criteria**. When the user asks about stories ("show me the login stories", "what's US_12"), start here.
|
|
73
|
+
- **Create MANUAL test cases for a story (YOU draft them).** Read the story's acceptance criteria with \`story_get\`, then author manual test cases that COVER them — a happy-path case plus the negative / edge cases the criteria imply. Draft each with **\`tc_draft_create({ title, category: "manual", storyRef: "<story id or US_ key>", steps: [...] })\`**. Each manual step is PLAIN PROSE — a clear \`description\` and a non-empty \`expectedResult\`, \`actionType: "manual"\`, \`stepType: "web"\`; **no locators** (manual tests aren't executable until automated). \`storyRef\` links every case back to the story. Show the drafted cases in plain language; revise with \`tc_draft_update\` on request.
|
|
74
|
+
- **SAVING IS ALWAYS AN EXPLICIT USER ACTION — never save on your own.** A drafted manual test case stays an UNSAVED draft. Call **\`tc_save({ draftId })\`** ONLY when the user EXPLICITLY tells you to (e.g. "save it", "save these", "persist it", "okay save"). Do NOT save just because you finished drafting or the user said the draft "looks good" — wait for an explicit save instruction. The user may instead review and **save it themselves in the Walnut UI** — that's fine; do NOT also \`tc_save\` (it would duplicate). Until an explicit save (in a prompt, or done in the UI), leave every drafted test case unsaved.
|
|
75
|
+
- **Automate a manual test case (promote IN PLACE)** — **\`tc_automate({ tcId, url, extra? })\`** drives a REAL browser recording using the manual steps as the guide, then REPLACES that test case's steps with the recorded ones (real locators) and flips it to \`automated\`, keeping the story link. The manual steps are **superseded**. First ASK the user for the app **URL** and any **credentials / data / assertions** (pass them as \`url\` + \`extra\`); confirm before automating, since it overwrites the manual steps. \`profile\` reuses a signed-in browser profile.
|
|
76
|
+
|
|
77
|
+
## Run — execute & triage
|
|
78
|
+
1. **\`tc_run\` needs \`mode\`: \`local\` or \`cloud\` — ASK if the user hasn't said.** local = the user's own agent browser; cloud → first check \`cloud_runners\`, and if none are online say *"there are no active cloud runners in your project"* and stop (don't silently fall back to local).
|
|
79
|
+
2. **\`tc_run({ tcId, mode })\`** → a run id; then **poll \`run_status\`** until terminal (passed / failed / stopped / error). Don't call a still-\`queued\`/\`running\` run a result; if it stayed \`queued\` and never started, the agent likely didn't pick it up — say that, not "pass".
|
|
80
|
+
3. **Report** plainly: pass / fail, step counts, the FAILING step + its error, the report link. **Triage, don't fix** — a broken locator or changed app → offer to re-delegate that flow to the sub-agent. \`run_stop\` cancels a run.
|
|
81
|
+
4. **Suites:** **\`suite_run({ suiteId, mode })\`** executes a whole suite (its member test cases, in order) — same \`local\`/\`cloud\` rule + ASK. It returns a suite-run id you poll with the SAME **\`run_status\`** (it lists each test case's pass/fail) and cancel with **\`run_stop\`**.
|
|
82
|
+
5. **Suite membership:** **\`suite_add_testcases({ suiteId, tcIds })\`** and **\`suite_remove_testcases({ suiteId, tcIds })\`** add / remove test cases (find ids via \`suite_list\` + \`tc_list\`; confirm the target suite when the user names it loosely). A suite holds ONE category — all manual OR all automated; the product rejects a mix.
|
|
83
|
+
|
|
84
|
+
## Modify a test case
|
|
85
|
+
Edit an EXISTING test case's steps from here — no re-recording needed for structure.
|
|
86
|
+
- **\`journal_grep({ tcId, pattern? })\`** FIRST — shows the NUMBERED steps (the numbers the edit ops use); \`pattern\` filters to locate a range.
|
|
87
|
+
- **\`journal_modify({ tcId, ops })\`** — apply edits, opens the result in the editor. Ops: \`wrap_in_loop { from, to }\` / \`wrap_in_if { from, to }\` (wrap that step range), \`insert_nested { at, tcId }\` (reuse an existing TC as a nested step), \`add_step\` / \`update_step\` / \`remove_step\` / \`move_step\`, and \`set_meta { patch: { title?, description?, priority?, … } }\` to RENAME / edit test-case metadata.
|
|
88
|
+
- **\`tc_delete({ tcId })\`** — delete a test case (recoverable from the product recycle bin).
|
|
89
|
+
- Loop / if-else / nested / remove / move / edit-existing all persist cleanly. Adding a brand-new INTERACTION step (click/type a NEW element) has no captured locator → it's a manual, non-executable step; to add a real interaction, RECORD it instead.
|
|
90
|
+
|
|
91
|
+
## Always
|
|
92
|
+
- **NEVER save on your own — \`tc_save\` is an EXPLICIT user action ONLY.** A test case (recorded draft OR manual draft) is persisted only when the user EXPLICITLY says so (e.g. "save this test case", "save it", "persist it", "okay save"). Finishing a recording, or the user approving the CONTENT ("looks good", "nice", "correct"), is NOT a save instruction — if unsure whether a reply meant save, ASK rather than saving. The user may instead save it themselves in the Walnut UI; then do NOT also \`tc_save\` (it would duplicate).
|
|
93
|
+
- One door: you call the lifecycle tools + the technology sub-agents; the product and which technology are resolved downstream. Delegate with a complete, self-contained task. Never fabricate — read every result from the tools. Show steps in plain language, not code.`,
|
|
94
|
+
};
|
|
95
|
+
const AGENTS = [COMMON_QA_AGENT];
|
|
96
|
+
const ALL_LOOPS = ['claude', 'cursor', 'windsurf', 'vscode', 'codex'];
|
|
97
|
+
const yamlList = (items) => items.map((t) => ` - ${t}`).join('\n');
|
|
98
|
+
const toolsNote = (a) => `\n\n## Allowed QA tools\nUse only these:\n${a.tools.map((t) => `- \`${t}\``).join('\n')}\n`;
|
|
99
|
+
/** One agent → { path, content } for a per-file client. */
|
|
100
|
+
function fileFor(loop, a) {
|
|
101
|
+
switch (loop) {
|
|
102
|
+
case 'claude': // Claude Code subagent — native name/model/color/tools frontmatter (machine-enforced)
|
|
103
|
+
return {
|
|
104
|
+
path: `.claude/agents/${a.slug}.md`,
|
|
105
|
+
content: `---\nname: ${a.slug}\ndescription: ${a.description}\nmodel: ${a.model}\ncolor: ${a.color}\ntools:\n${yamlList(a.tools)}\n---\n\n${a.body}\n`,
|
|
106
|
+
};
|
|
107
|
+
case 'cursor': // Cursor project rule (.mdc)
|
|
108
|
+
return {
|
|
109
|
+
path: `.cursor/rules/${a.slug}.mdc`,
|
|
110
|
+
content: `---\ndescription: ${a.description}\nalwaysApply: false\n---\n\n${a.body}${toolsNote(a)}`,
|
|
111
|
+
};
|
|
112
|
+
case 'windsurf': // Windsurf rule
|
|
113
|
+
return {
|
|
114
|
+
path: `.windsurf/rules/${a.slug}.md`,
|
|
115
|
+
content: `---\ntrigger: model_decision\ndescription: ${a.description}\n---\n\n${a.body}${toolsNote(a)}`,
|
|
116
|
+
};
|
|
117
|
+
case 'vscode': // VS Code custom AGENT (not a chat mode)
|
|
118
|
+
return {
|
|
119
|
+
path: `.github/agents/${a.slug}.agent.md`,
|
|
120
|
+
content: `---\ndescription: ${a.description}\ntools: [${a.tools.map((t) => `'${t}'`).join(', ')}]\n---\n\n${a.body}\n`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function parseLoop(args) {
|
|
125
|
+
for (let i = 0; i < args.length; i++) {
|
|
126
|
+
const a = args[i];
|
|
127
|
+
if (a.startsWith('--loop='))
|
|
128
|
+
return a.slice('--loop='.length);
|
|
129
|
+
if (a === '--loop')
|
|
130
|
+
return args[i + 1] ?? null;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
function writeFile(cwd, rel, content, loop) {
|
|
135
|
+
const target = join(cwd, rel);
|
|
136
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
137
|
+
writeFileSync(target, content, 'utf8');
|
|
138
|
+
process.stderr.write(`init-agents [${loop}]: wrote ${rel}\n`);
|
|
139
|
+
}
|
|
140
|
+
export async function runInitAgents(args) {
|
|
141
|
+
const loopArg = parseLoop(args) ?? 'all'; // default: expose the agent to ALL AI clients
|
|
142
|
+
const valid = [...ALL_LOOPS, 'all'];
|
|
143
|
+
if (!valid.includes(loopArg)) {
|
|
144
|
+
process.stderr.write(`init-agents: --loop must be one of <${valid.join(' | ')}> (default: all)\n`);
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const loops = loopArg === 'all' ? [...ALL_LOOPS] : [loopArg];
|
|
149
|
+
const cwd = process.cwd();
|
|
150
|
+
for (const loop of loops) {
|
|
151
|
+
if (loop === 'codex') {
|
|
152
|
+
// Codex / generic AGENTS.md — one repo-wide file holding the single Common QA agent.
|
|
153
|
+
const rel = 'AGENTS.md';
|
|
154
|
+
const body = '# Common QA agent (common-qa-mcp)\n\nOne agent drives the Common QA MCP — the whole test-case lifecycle.\n\n' +
|
|
155
|
+
AGENTS.map((a) => `---\n\n## ${a.slug}\n\n${a.description}\n\n${a.body}${toolsNote(a)}`).join('\n');
|
|
156
|
+
let target = join(cwd, rel);
|
|
157
|
+
// Overwrite OUR own AGENTS.md in place (marker check); only sidestep a FOREIGN AGENTS.md so we
|
|
158
|
+
// never clobber a repo's real one. The marker is `common-qa-mcp` — every generated AGENTS.md
|
|
159
|
+
// carries it (in the tool allowlists), so re-running converges on a single clean file.
|
|
160
|
+
if (existsSync(target) && !readFileSync(target, 'utf8').includes('common-qa-mcp')) {
|
|
161
|
+
target = join(cwd, 'common-qa-mcp.AGENTS.md');
|
|
162
|
+
process.stderr.write('init-agents [codex]: a foreign AGENTS.md exists — writing common-qa-mcp.AGENTS.md instead; merge it in.\n');
|
|
163
|
+
}
|
|
164
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
165
|
+
writeFileSync(target, body, 'utf8');
|
|
166
|
+
process.stderr.write(`init-agents [codex]: wrote ${target}\n`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
for (const a of AGENTS) {
|
|
170
|
+
const { path, content } = fileFor(loop, a);
|
|
171
|
+
writeFile(cwd, path, content, loop);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
process.stderr.write('\nInstalled the Common QA agent. Ensure the "common-qa-mcp" MCP server is configured in this ' +
|
|
175
|
+
'client (command: npx, args: ["-y","common-qa-mcp"], env: QA_MCP_TOKEN / QA_MCP_URL / QA_MCP_PRODUCT), ' +
|
|
176
|
+
'reload the client so it picks up the agent, then start a QA task.\n');
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=initAgents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"initAgents.js","sourceRoot":"","sources":["../src/initAgents.ts"],"names":[],"mappings":"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"}
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deliberately NOT the agent's `createLogger` (src/utils/logger.ts): that logger's
|
|
3
|
+
* `info` level writes through `console.log`, i.e. stdout. This module backs both
|
|
4
|
+
* (a) the in-process `/mcp` HTTP mount and (b) the standalone stdio dev harness
|
|
5
|
+
* (src/mcp/qa/index.ts) kept importable for local debugging -- and for (b), stdout
|
|
6
|
+
* IS the MCP transport itself, so anything written there corrupts the protocol
|
|
7
|
+
* stream. `console.error` (stderr) is safe for both callers and costs nothing to
|
|
8
|
+
* keep that way, so this stays a tiny local logger rather than adopting the
|
|
9
|
+
* agent-wide one. Never pass a token/secret to this -- callers must go through
|
|
10
|
+
* `mask()` first.
|
|
11
|
+
*/
|
|
12
|
+
export declare const log: (...a: unknown[]) => void;
|
|
13
|
+
export declare const mask: (t?: string) => string;
|
|
14
|
+
//# sourceMappingURL=log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,GAAG,GAAI,GAAG,GAAG,OAAO,EAAE,SAAoC,CAAC;AACxE,eAAO,MAAM,IAAI,GAAI,IAAI,MAAM,WAA0C,CAAC"}
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deliberately NOT the agent's `createLogger` (src/utils/logger.ts): that logger's
|
|
3
|
+
* `info` level writes through `console.log`, i.e. stdout. This module backs both
|
|
4
|
+
* (a) the in-process `/mcp` HTTP mount and (b) the standalone stdio dev harness
|
|
5
|
+
* (src/mcp/qa/index.ts) kept importable for local debugging -- and for (b), stdout
|
|
6
|
+
* IS the MCP transport itself, so anything written there corrupts the protocol
|
|
7
|
+
* stream. `console.error` (stderr) is safe for both callers and costs nothing to
|
|
8
|
+
* keep that way, so this stays a tiny local logger rather than adopting the
|
|
9
|
+
* agent-wide one. Never pass a token/secret to this -- callers must go through
|
|
10
|
+
* `mask()` first.
|
|
11
|
+
*/
|
|
12
|
+
export const log = (...a) => console.error('[qa-mcp]', ...a);
|
|
13
|
+
export const mask = (t) => (t ? t.slice(0, 6) + '…' : '(unset)');
|
|
14
|
+
//# sourceMappingURL=log.js.map
|
package/dist/log.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,CAAY,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC;AACxE,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CallToolResult helpers. Lives outside server.ts so tool modules
|
|
3
|
+
* (src/tools/*.ts) can import it without a circular dependency back on
|
|
4
|
+
* server.ts, which itself registers those tool modules.
|
|
5
|
+
*/
|
|
6
|
+
export declare const err: (text: string) => {
|
|
7
|
+
isError: true;
|
|
8
|
+
content: {
|
|
9
|
+
type: "text";
|
|
10
|
+
text: string;
|
|
11
|
+
}[];
|
|
12
|
+
};
|
|
13
|
+
export declare const ok: (text: string, structuredContent?: Record<string, unknown>) => {
|
|
14
|
+
structuredContent?: Record<string, unknown>;
|
|
15
|
+
content: {
|
|
16
|
+
type: "text";
|
|
17
|
+
text: string;
|
|
18
|
+
}[];
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=mcpResult.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpResult.d.ts","sourceRoot":"","sources":["../src/mcpResult.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,GAAG,GAAI,MAAM,MAAM;;;;;;CAAmG,CAAC;AACpI,eAAO,MAAM,EAAE,GAAI,MAAM,MAAM,EAAE,oBAAoB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;CAC2B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CallToolResult helpers. Lives outside server.ts so tool modules
|
|
3
|
+
* (src/tools/*.ts) can import it without a circular dependency back on
|
|
4
|
+
* server.ts, which itself registers those tool modules.
|
|
5
|
+
*/
|
|
6
|
+
export const err = (text) => ({ isError: true, content: [{ type: 'text', text: `### Error\n${text}` }] });
|
|
7
|
+
export const ok = (text, structuredContent) => ({ content: [{ type: 'text', text }], ...(structuredContent ? { structuredContent } : {}) });
|
|
8
|
+
//# sourceMappingURL=mcpResult.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpResult.js","sourceRoot":"","sources":["../src/mcpResult.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAa,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,cAAc,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;AACpI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,iBAA2C,EAAE,EAAE,CAC9E,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC"}
|
package/dist/mount.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Mounts the Common QA MCP at POST/GET/DELETE /mcp on an Express app -- the STANDALONE gateway path
|
|
4
|
+
* (the Walnut agent has its own mount that also wires the browser-recording/editor tools).
|
|
5
|
+
*
|
|
6
|
+
* MUST be registered before any body parser: each handler reads the raw request body itself and
|
|
7
|
+
* always terminates the request. One StreamableHTTPServerTransport (+ one McpServer) per MCP
|
|
8
|
+
* session, keyed by the `mcp-session-id` header the transport generates on `initialize`. DraftStore
|
|
9
|
+
* is a single shared instance so a draft survives a client reconnect.
|
|
10
|
+
*/
|
|
11
|
+
export declare function mountQaMcp(app: Express): void;
|
|
12
|
+
//# sourceMappingURL=mount.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAa1D;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CA2G7C"}
|
package/dist/mount.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { buildServer } from './server.js';
|
|
5
|
+
import { QaConnection } from './connection.js';
|
|
6
|
+
import { DraftStore } from './drafts/store.js';
|
|
7
|
+
import { createAdapter } from './adapters/factory.js';
|
|
8
|
+
import { resolveConfig } from './config.js';
|
|
9
|
+
import { connectionFromRequest } from './requestConnection.js';
|
|
10
|
+
import { log } from './log.js';
|
|
11
|
+
/**
|
|
12
|
+
* Mounts the Common QA MCP at POST/GET/DELETE /mcp on an Express app -- the STANDALONE gateway path
|
|
13
|
+
* (the Walnut agent has its own mount that also wires the browser-recording/editor tools).
|
|
14
|
+
*
|
|
15
|
+
* MUST be registered before any body parser: each handler reads the raw request body itself and
|
|
16
|
+
* always terminates the request. One StreamableHTTPServerTransport (+ one McpServer) per MCP
|
|
17
|
+
* session, keyed by the `mcp-session-id` header the transport generates on `initialize`. DraftStore
|
|
18
|
+
* is a single shared instance so a draft survives a client reconnect.
|
|
19
|
+
*/
|
|
20
|
+
export function mountQaMcp(app) {
|
|
21
|
+
const config = resolveConfig(process.env);
|
|
22
|
+
const adapter = createAdapter(config);
|
|
23
|
+
const connection = new QaConnection(config);
|
|
24
|
+
const drafts = new DraftStore();
|
|
25
|
+
const deps = {
|
|
26
|
+
adapter,
|
|
27
|
+
connection,
|
|
28
|
+
mode: config.mode,
|
|
29
|
+
drafts,
|
|
30
|
+
walnutUiOrigin: config.walnutUiOrigin,
|
|
31
|
+
agentUrl: config.agentUrl,
|
|
32
|
+
// The standalone gateway has no Electron window -- tc_draft_open just returns the editor link.
|
|
33
|
+
openEditorWindow: async () => false,
|
|
34
|
+
};
|
|
35
|
+
const transports = new Map();
|
|
36
|
+
function readJsonBody(req) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let data = '';
|
|
39
|
+
req.setEncoding('utf8');
|
|
40
|
+
req.on('data', (chunk) => { data += chunk; });
|
|
41
|
+
req.on('end', () => {
|
|
42
|
+
if (!data)
|
|
43
|
+
return resolve(undefined);
|
|
44
|
+
try {
|
|
45
|
+
resolve(JSON.parse(data));
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
req.on('error', reject);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function jsonRpcError(res, status, code, message) {
|
|
55
|
+
res.status(status).json({ jsonrpc: '2.0', error: { code, message }, id: null });
|
|
56
|
+
}
|
|
57
|
+
const postHandler = async (req, res) => {
|
|
58
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
59
|
+
try {
|
|
60
|
+
const existing = sessionId ? transports.get(sessionId) : undefined;
|
|
61
|
+
if (existing) {
|
|
62
|
+
const parsedBody = await readJsonBody(req);
|
|
63
|
+
await existing.handleRequest(req, res, parsedBody);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (sessionId) {
|
|
67
|
+
jsonRpcError(res, 404, -32001, 'Session not found');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const parsedBody = await readJsonBody(req);
|
|
71
|
+
if (!isInitializeRequest(parsedBody)) {
|
|
72
|
+
jsonRpcError(res, 400, -32000, 'Bad Request: No valid session ID provided');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const newTransport = new StreamableHTTPServerTransport({
|
|
76
|
+
sessionIdGenerator: () => randomUUID(),
|
|
77
|
+
onsessioninitialized: (id) => { transports.set(id, newTransport); },
|
|
78
|
+
});
|
|
79
|
+
newTransport.onclose = () => {
|
|
80
|
+
const id = newTransport.sessionId;
|
|
81
|
+
if (id)
|
|
82
|
+
transports.delete(id);
|
|
83
|
+
};
|
|
84
|
+
const server = buildServer(deps);
|
|
85
|
+
await server.connect(newTransport);
|
|
86
|
+
// Config-based connection: if the client supplied a token via transport headers/URL (the
|
|
87
|
+
// no-chat mcp.json setup) AND we're offline, prime the shared connection. Fire-and-forget so a
|
|
88
|
+
// slow backend never stalls the handshake; a runtime qa_connect is not clobbered on reconnect.
|
|
89
|
+
if (connection.describe().mode === 'offline') {
|
|
90
|
+
const primeReq = connectionFromRequest(req);
|
|
91
|
+
if (primeReq) {
|
|
92
|
+
void connection.connect(primeReq).then((r) => log(r.ok
|
|
93
|
+
? `connection primed from request (product=${primeReq.product ?? 'walnut'})`
|
|
94
|
+
: `header/query connect failed: ${r.error}`), (e) => log('header/query connect threw', e instanceof Error ? e.message : String(e)));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
await newTransport.handleRequest(req, res, parsedBody);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
log('POST /mcp error', e instanceof Error ? e.message : String(e));
|
|
101
|
+
if (!res.headersSent)
|
|
102
|
+
jsonRpcError(res, 500, -32603, 'Internal server error');
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const streamHandler = async (req, res) => {
|
|
106
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
107
|
+
const transport = sessionId ? transports.get(sessionId) : undefined;
|
|
108
|
+
if (!transport) {
|
|
109
|
+
res.status(400).send('Invalid or missing session ID');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
await transport.handleRequest(req, res);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
log(`${req.method} /mcp error`, e instanceof Error ? e.message : String(e));
|
|
117
|
+
if (!res.headersSent)
|
|
118
|
+
res.status(500).send('Error processing request');
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
app.post('/mcp', postHandler);
|
|
122
|
+
app.get('/mcp', streamHandler);
|
|
123
|
+
app.delete('/mcp', streamHandler);
|
|
124
|
+
log(`mounted at /mcp (mode=${config.mode})`);
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=mount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount.js","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** The projectId last selected for `url`, or undefined when none was persisted. */
|
|
2
|
+
export declare function readLastProject(url: string): string | undefined;
|
|
3
|
+
/** Remember `projectId` as the last selection for `url`. Best-effort — never throws. */
|
|
4
|
+
export declare function writeLastProject(url: string, projectId: string): void;
|
|
5
|
+
//# sourceMappingURL=projectStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectStore.d.ts","sourceRoot":"","sources":["../src/projectStore.ts"],"names":[],"mappings":"AAyCA,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI/D;AAED,wFAAwF;AACxF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAWrE"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persist the LAST-SELECTED projectId across restarts.
|
|
3
|
+
*
|
|
4
|
+
* WHY. The connection boots from the startup config (env / ~/.common-qa-mcp/config.json), which
|
|
5
|
+
* carries a URL + token but usually NO project — so a client relaunch drops the connection back to
|
|
6
|
+
* project-less. That is not just cosmetic: downstream, AI recording resolves its LLM model pool PER
|
|
7
|
+
* PROJECT, so a project-less connection makes `web({task})` fail with "no LLM model configured for
|
|
8
|
+
* this project". Remembering the last project makes a relaunch transparent — no re-`qa_switch_project`.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IT STORES. ONLY the projectId, keyed by backend URL. Never the token — the connection module
|
|
11
|
+
* keeps its deliberate "no credential on disk" contract (connection.ts), and a project id is a
|
|
12
|
+
* non-secret identifier. Keying by URL means a remembered project never leaks across origins, and
|
|
13
|
+
* switching backends is remembered independently.
|
|
14
|
+
*
|
|
15
|
+
* Best-effort throughout: every read/write failure is swallowed, so a missing or unwritable home
|
|
16
|
+
* directory can never break a connection or a project switch.
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import os from 'os';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { createLogger } from './vendor/logger.js';
|
|
22
|
+
const logger = createLogger('QaProjectStore');
|
|
23
|
+
const storePath = () => path.join(os.homedir(), '.common-qa-mcp', 'last-project.json');
|
|
24
|
+
/** Stable key: drop a trailing slash and lowercase, so the same backend maps to one entry. */
|
|
25
|
+
const keyFor = (url) => url.replace(/\/+$/, '').toLowerCase();
|
|
26
|
+
function readStore() {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(fs.readFileSync(storePath(), 'utf8'));
|
|
29
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* absent / malformed → empty */
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
/** The projectId last selected for `url`, or undefined when none was persisted. */
|
|
38
|
+
export function readLastProject(url) {
|
|
39
|
+
if (!url)
|
|
40
|
+
return undefined;
|
|
41
|
+
const val = readStore()[keyFor(url)];
|
|
42
|
+
return typeof val === 'string' && val.trim() ? val : undefined;
|
|
43
|
+
}
|
|
44
|
+
/** Remember `projectId` as the last selection for `url`. Best-effort — never throws. */
|
|
45
|
+
export function writeLastProject(url, projectId) {
|
|
46
|
+
if (!url || !projectId)
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(path.dirname(storePath()), { recursive: true });
|
|
50
|
+
const store = readStore();
|
|
51
|
+
if (store[keyFor(url)] === projectId)
|
|
52
|
+
return; // no-op write avoided
|
|
53
|
+
store[keyFor(url)] = projectId;
|
|
54
|
+
fs.writeFileSync(storePath(), JSON.stringify(store, null, 2), 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logger.warn(`could not persist last project (${err instanceof Error ? err.message : String(err)})`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=projectStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectStore.js","sourceRoot":"","sources":["../src/projectStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,MAAM,GAAG,YAAY,CAAC,gBAAgB,CAAC,CAAC;AAE9C,MAAM,SAAS,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,gBAAgB,EAAE,mBAAmB,CAAC,CAAC;AAE/F,8FAA8F;AAC9F,MAAM,MAAM,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;AAI9E,SAAS,SAAS;IAChB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,EAAE,MAAM,CAAC,CAAY,CAAC;QAC3E,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;YAAE,OAAO,MAAe,CAAC;IAC7F,CAAC;IAAC,MAAM,CAAC;QACP,gCAAgC;IAClC,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,GAAG,GAAG,SAAS,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACrC,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AACjE,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,SAAiB;IAC7D,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS;QAAE,OAAO;IAC/B,IAAI,CAAC;QACH,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;QAC1B,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,SAAS;YAAE,OAAO,CAAC,sBAAsB;QACpE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC;QAC/B,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACxE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,mCAAmC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACtG,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
import type { ConnectRequest } from './connection.js';
|
|
3
|
+
/**
|
|
4
|
+
* Config-based connection (the no-chat setup path): an AI client's mcp.json can carry the
|
|
5
|
+
* connection in the transport itself instead of the user calling `qa_connect` in chat, e.g.
|
|
6
|
+
* { "url": "http://127.0.0.1:4050/mcp",
|
|
7
|
+
* "headers": { "Authorization": "Bearer <token>", "x-qa-product": "walnut",
|
|
8
|
+
* "x-qa-url": "http://localhost:8080", "x-project-id": "<id>" } }
|
|
9
|
+
*
|
|
10
|
+
* - token <- `Authorization: Bearer …` (scheme stripped; `x-qa-token` also accepted)
|
|
11
|
+
* - url <- `?url=` | `x-qa-url` header | agent env QA_MCP_URL (the BACKEND / SQA-MCP url,
|
|
12
|
+
* NOT this /mcp endpoint)
|
|
13
|
+
* - product <- `?product=` | `x-qa-product` | QA_MCP_PROVIDER (default 'walnut')
|
|
14
|
+
* - projectId<- `?projectId=` | `x-project-id` | QA_MCP_PROJECT_ID
|
|
15
|
+
* - auth <- `?auth=` | `x-qa-auth` ('jwt' | 'key')
|
|
16
|
+
*
|
|
17
|
+
* Returns null when there is no usable token+url, so the offline authoring path is untouched.
|
|
18
|
+
*/
|
|
19
|
+
export declare function connectionFromRequest(req: Request): ConnectRequest | null;
|
|
20
|
+
//# sourceMappingURL=requestConnection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requestConnection.d.ts","sourceRoot":"","sources":["../src/requestConnection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAetD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAYzE"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Type-only imports above (erased at runtime), so this module has NO runtime dependency graph --
|
|
2
|
+
// it's a pure parser and can be unit-tested without pulling mount.ts's Electron/browser imports.
|
|
3
|
+
function firstHeader(req, name) {
|
|
4
|
+
const v = req.headers[name];
|
|
5
|
+
return Array.isArray(v) ? v[0] : v;
|
|
6
|
+
}
|
|
7
|
+
function firstQuery(req, name) {
|
|
8
|
+
const v = req.query[name];
|
|
9
|
+
if (Array.isArray(v))
|
|
10
|
+
return v[0] != null ? String(v[0]) : undefined;
|
|
11
|
+
return v != null ? String(v) : undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Config-based connection (the no-chat setup path): an AI client's mcp.json can carry the
|
|
15
|
+
* connection in the transport itself instead of the user calling `qa_connect` in chat, e.g.
|
|
16
|
+
* { "url": "http://127.0.0.1:4050/mcp",
|
|
17
|
+
* "headers": { "Authorization": "Bearer <token>", "x-qa-product": "walnut",
|
|
18
|
+
* "x-qa-url": "http://localhost:8080", "x-project-id": "<id>" } }
|
|
19
|
+
*
|
|
20
|
+
* - token <- `Authorization: Bearer …` (scheme stripped; `x-qa-token` also accepted)
|
|
21
|
+
* - url <- `?url=` | `x-qa-url` header | agent env QA_MCP_URL (the BACKEND / SQA-MCP url,
|
|
22
|
+
* NOT this /mcp endpoint)
|
|
23
|
+
* - product <- `?product=` | `x-qa-product` | QA_MCP_PROVIDER (default 'walnut')
|
|
24
|
+
* - projectId<- `?projectId=` | `x-project-id` | QA_MCP_PROJECT_ID
|
|
25
|
+
* - auth <- `?auth=` | `x-qa-auth` ('jwt' | 'key')
|
|
26
|
+
*
|
|
27
|
+
* Returns null when there is no usable token+url, so the offline authoring path is untouched.
|
|
28
|
+
*/
|
|
29
|
+
export function connectionFromRequest(req) {
|
|
30
|
+
const authz = firstHeader(req, 'authorization') ?? '';
|
|
31
|
+
const token = ((authz.replace(/^Bearer\s+/i, '')) || firstHeader(req, 'x-qa-token') || '').trim();
|
|
32
|
+
if (!token)
|
|
33
|
+
return null;
|
|
34
|
+
const url = (firstQuery(req, 'url') ?? firstHeader(req, 'x-qa-url') ?? process.env['QA_MCP_URL'] ?? '').trim();
|
|
35
|
+
if (!url)
|
|
36
|
+
return null;
|
|
37
|
+
const productRaw = firstQuery(req, 'product') ?? firstHeader(req, 'x-qa-product') ?? process.env['QA_MCP_PROVIDER'] ?? '';
|
|
38
|
+
const product = productRaw === 'simplifyqa' ? 'simplifyqa' : 'walnut';
|
|
39
|
+
const projectId = (firstQuery(req, 'projectId') ?? firstHeader(req, 'x-project-id') ?? process.env['QA_MCP_PROJECT_ID'] ?? '').trim();
|
|
40
|
+
const authRaw = firstQuery(req, 'auth') ?? firstHeader(req, 'x-qa-auth') ?? '';
|
|
41
|
+
const auth = authRaw === 'key' ? 'key' : authRaw === 'jwt' ? 'jwt' : undefined;
|
|
42
|
+
return { url, token, product, ...(projectId ? { projectId } : {}), ...(auth ? { auth } : {}) };
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=requestConnection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requestConnection.js","sourceRoot":"","sources":["../src/requestConnection.ts"],"names":[],"mappings":"AAGA,iGAAiG;AACjG,iGAAiG;AAEjG,SAAS,WAAW,CAAC,GAAY,EAAE,IAAY;IAC7C,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC;AACD,SAAS,UAAU,CAAC,GAAY,EAAE,IAAY;IAC5C,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACrE,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3C,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAY;IAChD,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,eAAe,CAAC,IAAI,EAAE,CAAC;IACtD,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,IAAI,WAAW,CAAC,GAAG,EAAE,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAClG,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,WAAW,CAAC,GAAG,EAAE,UAAU,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/G,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,IAAI,WAAW,CAAC,GAAG,EAAE,cAAc,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC;IAC1H,MAAM,OAAO,GAA4B,UAAU,KAAK,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC/F,MAAM,SAAS,GAAG,CAAC,UAAU,CAAC,GAAG,EAAE,WAAW,CAAC,IAAI,WAAW,CAAC,GAAG,EAAE,cAAc,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACtI,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,WAAW,CAAC,GAAG,EAAE,WAAW,CAAC,IAAI,EAAE,CAAC;IAC/E,MAAM,IAAI,GAA8B,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1G,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACjG,CAAC"}
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PRODUCT stdio entry — what `common-qa-mcp` (or `common-qa-mcp serve`) runs when an AI client
|
|
3
|
+
* launches it over stdio (the npx path). Mirrors mount.ts's HTTP wiring (a live QaConnection so
|
|
4
|
+
* qa_connect works + a shared DraftStore so drafts survive across a session) but the transport is
|
|
5
|
+
* stdio and the connection is primed from the `QA_MCP_*` env the client passes in its mcp config —
|
|
6
|
+
* there are no HTTP headers over stdio. All logging goes to STDERR (log.ts); stdout is the MCP
|
|
7
|
+
* transport itself and must stay clean.
|
|
8
|
+
*/
|
|
9
|
+
export declare function startStdioServer(): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=serve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"AAQA;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAmBtD"}
|