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
package/dist/config.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const DEFAULT_CAPS = ['tc', 'suites', 'runs', 'objects', 'alm'];
|
|
5
|
+
function asProvider(v) {
|
|
6
|
+
return v === 'walnut' || v === 'simplifyqa' ? v : undefined;
|
|
7
|
+
}
|
|
8
|
+
function asWalnutAuth(v) {
|
|
9
|
+
return v === 'key' || v === 'jwt' ? v : undefined;
|
|
10
|
+
}
|
|
11
|
+
function asCaps(v) {
|
|
12
|
+
if (v == null)
|
|
13
|
+
return undefined;
|
|
14
|
+
const parts = Array.isArray(v) ? v.map(String) : String(v).split(',');
|
|
15
|
+
const cleaned = parts.map((s) => s.trim()).filter(Boolean);
|
|
16
|
+
return cleaned.length ? new Set(cleaned) : undefined;
|
|
17
|
+
}
|
|
18
|
+
function asPort(v) {
|
|
19
|
+
if (v == null || v === '')
|
|
20
|
+
return undefined;
|
|
21
|
+
const n = Number(v);
|
|
22
|
+
return Number.isFinite(n) ? n : undefined;
|
|
23
|
+
}
|
|
24
|
+
function asString(v) {
|
|
25
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
26
|
+
}
|
|
27
|
+
// Shared normalizer for the two raw, loosely-typed sources (env vars and the
|
|
28
|
+
// JSON config file) that both describe a connection using the same field set.
|
|
29
|
+
function normalizeLayer(raw) {
|
|
30
|
+
const layer = {};
|
|
31
|
+
const provider = asProvider(raw['provider']);
|
|
32
|
+
if (provider)
|
|
33
|
+
layer.provider = provider;
|
|
34
|
+
const url = asString(raw['url']);
|
|
35
|
+
if (url)
|
|
36
|
+
layer.url = url;
|
|
37
|
+
const token = asString(raw['token']);
|
|
38
|
+
if (token)
|
|
39
|
+
layer.token = token;
|
|
40
|
+
const projectId = asString(raw['projectId']);
|
|
41
|
+
if (projectId)
|
|
42
|
+
layer.projectId = projectId;
|
|
43
|
+
const customerId = asString(raw['customerId']);
|
|
44
|
+
if (customerId)
|
|
45
|
+
layer.customerId = customerId;
|
|
46
|
+
const moduleId = asString(raw['moduleId']);
|
|
47
|
+
if (moduleId)
|
|
48
|
+
layer.moduleId = moduleId;
|
|
49
|
+
const walnutAuth = asWalnutAuth(raw['walnutAuth']);
|
|
50
|
+
if (walnutAuth)
|
|
51
|
+
layer.walnutAuth = walnutAuth;
|
|
52
|
+
const caps = asCaps(raw['caps']);
|
|
53
|
+
if (caps)
|
|
54
|
+
layer.caps = caps;
|
|
55
|
+
const port = asPort(raw['port']);
|
|
56
|
+
if (port !== undefined)
|
|
57
|
+
layer.port = port;
|
|
58
|
+
const walnutUiOrigin = asString(raw['walnutUiOrigin']);
|
|
59
|
+
if (walnutUiOrigin)
|
|
60
|
+
layer.walnutUiOrigin = walnutUiOrigin;
|
|
61
|
+
const agentUrl = asString(raw['agentUrl']);
|
|
62
|
+
if (agentUrl)
|
|
63
|
+
layer.agentUrl = agentUrl;
|
|
64
|
+
return layer;
|
|
65
|
+
}
|
|
66
|
+
function parseEnvLayer(env) {
|
|
67
|
+
return normalizeLayer({
|
|
68
|
+
provider: env['QA_MCP_PROVIDER'],
|
|
69
|
+
url: env['QA_MCP_URL'],
|
|
70
|
+
token: env['QA_MCP_TOKEN'],
|
|
71
|
+
projectId: env['QA_MCP_PROJECT_ID'],
|
|
72
|
+
customerId: env['QA_MCP_CUSTOMER_ID'],
|
|
73
|
+
moduleId: env['QA_MCP_MODULE_ID'],
|
|
74
|
+
walnutAuth: env['QA_MCP_WALNUT_AUTH'],
|
|
75
|
+
caps: env['QA_MCP_CAPS'],
|
|
76
|
+
port: env['QA_MCP_PORT'],
|
|
77
|
+
walnutUiOrigin: env['QA_MCP_WALNUT_UI'],
|
|
78
|
+
agentUrl: env['QA_MCP_AGENT_URL'],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Reads the named connection out of ~/.common-qa-mcp/config.json. An absent
|
|
82
|
+
// or malformed file (or a missing connection name) is just an empty layer —
|
|
83
|
+
// this must never throw, since it runs on every server boot.
|
|
84
|
+
function readConfigFileLayer(connectionName) {
|
|
85
|
+
try {
|
|
86
|
+
const filePath = path.join(os.homedir(), '.common-qa-mcp', 'config.json');
|
|
87
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
88
|
+
const parsed = JSON.parse(raw);
|
|
89
|
+
const name = connectionName ?? parsed.default ?? 'default';
|
|
90
|
+
const entry = parsed.connections?.[name];
|
|
91
|
+
return entry ? normalizeLayer(entry) : {};
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Copies only the keys `layer` actually defines onto `target`, so later,
|
|
98
|
+
// lower-priority-but-earlier-applied layers are never clobbered by absent keys.
|
|
99
|
+
function applyLayer(target, layer) {
|
|
100
|
+
for (const key of Object.keys(layer)) {
|
|
101
|
+
const value = layer[key];
|
|
102
|
+
if (value !== undefined)
|
|
103
|
+
target[key] = value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Only url + token are required to be CONNECTED. projectId is intentionally NOT here: a session may
|
|
107
|
+
// connect with no project selected, then the user picks one at runtime (list_projects ->
|
|
108
|
+
// qa_switch_project). Hardcoding a project in config defeats the "any project, any client" model.
|
|
109
|
+
const REQUIRED_FIELDS = [
|
|
110
|
+
['url', 'QA_MCP_URL'],
|
|
111
|
+
['token', 'QA_MCP_TOKEN'],
|
|
112
|
+
];
|
|
113
|
+
/**
|
|
114
|
+
* Resolution order (spec §12): defaults -> ~/.common-qa-mcp/config.json
|
|
115
|
+
* (named connections; cliFlags.connection selects) -> QA_MCP_* env -> CLI flags.
|
|
116
|
+
* Each layer only overrides keys it actually sets.
|
|
117
|
+
*
|
|
118
|
+
* Credentials (url/token) are optional overall:
|
|
119
|
+
* - BOTH url+token ABSENT -> OfflineQaConfig, no throw. The normal, supported way
|
|
120
|
+
* to run with no backend at all (author drafts, hand off to the editor).
|
|
121
|
+
* - ONE of url/token present but not the other -> misconfiguration, throws naming
|
|
122
|
+
* the missing var.
|
|
123
|
+
* - url+token present -> ConnectedQaConfig. projectId is OPTIONAL: absent means
|
|
124
|
+
* "connected, no project selected yet" and the user chooses one at runtime.
|
|
125
|
+
* Never logs `token` -- callers must go through `mask()` from log.ts if they log
|
|
126
|
+
* config at all.
|
|
127
|
+
*/
|
|
128
|
+
export function resolveConfig(env, cliFlags = {}) {
|
|
129
|
+
const merged = {
|
|
130
|
+
walnutAuth: 'key',
|
|
131
|
+
caps: new Set(DEFAULT_CAPS),
|
|
132
|
+
};
|
|
133
|
+
applyLayer(merged, readConfigFileLayer(cliFlags.connection));
|
|
134
|
+
applyLayer(merged, parseEnvLayer(env));
|
|
135
|
+
applyLayer(merged, cliFlags);
|
|
136
|
+
const missing = REQUIRED_FIELDS.filter(([key]) => !merged[key]).map(([, envVar]) => envVar);
|
|
137
|
+
if (missing.length === REQUIRED_FIELDS.length) {
|
|
138
|
+
// No credentials supplied at all -- offline is the intended, error-free outcome.
|
|
139
|
+
return { ...merged, mode: 'offline' };
|
|
140
|
+
}
|
|
141
|
+
if (missing.length) {
|
|
142
|
+
throw new Error(`Missing required configuration: set ${missing.join(', ')} (via environment variables, ` +
|
|
143
|
+
`~/.common-qa-mcp/config.json, or CLI flags).`);
|
|
144
|
+
}
|
|
145
|
+
return { ...merged, mode: 'connected' };
|
|
146
|
+
}
|
|
147
|
+
// A probe that throws (DNS failure, TLS failure, timeout, connection refused) is not a
|
|
148
|
+
// signal either way -- it collapses to "no response", same as never matching.
|
|
149
|
+
async function safeFetch(fetchImpl, url, init) {
|
|
150
|
+
try {
|
|
151
|
+
return await fetchImpl(url, init);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// A 200 only counts as a match -- for EITHER probe -- when it's actually JSON: a SPA
|
|
158
|
+
// catch-all (or any host that 200s everything, GET or POST) must not be mistaken for a
|
|
159
|
+
// product match just because it answered with 200. The content-type must say JSON AND the
|
|
160
|
+
// body must actually parse. Shared by both probes so neither gets a weaker bar than the other.
|
|
161
|
+
async function isJson200(res) {
|
|
162
|
+
if (!res || res.status !== 200)
|
|
163
|
+
return false;
|
|
164
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
165
|
+
if (!contentType.toLowerCase().includes('application/json'))
|
|
166
|
+
return false;
|
|
167
|
+
try {
|
|
168
|
+
await res.json();
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return false; // content-type lied -- body didn't actually parse as JSON
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Best-effort provider auto-detect (spec §3: explicit config always wins over this).
|
|
177
|
+
* Runs both probes concurrently (never short-circuits) and only returns a provider when
|
|
178
|
+
* exactly one of them gives a confident match. Returns null on any ambiguity -- including
|
|
179
|
+
* ties -- so the caller surfaces `describeAmbiguousProvider` instead of guessing.
|
|
180
|
+
*/
|
|
181
|
+
export async function detectProvider(url, fetchImpl = fetch) {
|
|
182
|
+
const [health, search] = await Promise.all([
|
|
183
|
+
safeFetch(fetchImpl, `${url}/api/health`),
|
|
184
|
+
safeFetch(fetchImpl, `${url}/pm/project/search`, { method: 'POST' }),
|
|
185
|
+
]);
|
|
186
|
+
// Symmetric rule for both probes: a 200 only counts when it's confirmed JSON (isJson200);
|
|
187
|
+
// a bare 401 only counts with corroboration; anything else (404, other statuses, a throw
|
|
188
|
+
// already collapsed to null by safeFetch) is no-match.
|
|
189
|
+
const isWalnut = await isJson200(health);
|
|
190
|
+
// A bare 401 on the search probe is only trusted as an SQA-specific signal when the
|
|
191
|
+
// health probe didn't hit the identical status -- if BOTH the walnut-only health route
|
|
192
|
+
// and the SQA search route 401 identically, that's a blanket auth gate on this host, not
|
|
193
|
+
// evidence of SQA (a genuine SQA host has no reason to 401 a route it doesn't implement).
|
|
194
|
+
// Walnut has no equivalent 401 path: its match is JSON-only, so it needs no such rule.
|
|
195
|
+
const isSimplifyQa = (await isJson200(search)) || (search?.status === 401 && health?.status !== 401);
|
|
196
|
+
if (isWalnut && !isSimplifyQa)
|
|
197
|
+
return 'walnut';
|
|
198
|
+
if (isSimplifyQa && !isWalnut)
|
|
199
|
+
return 'simplifyqa';
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The exact message a caller (the adapter factory, Task 5/6/11/12) should surface when
|
|
204
|
+
* `detectProvider` returns null -- spec §3: "on ambiguity, error asking for QA_MCP_PROVIDER".
|
|
205
|
+
* `detectProvider` itself never throws; this is a pure formatter for whoever does.
|
|
206
|
+
*/
|
|
207
|
+
export function describeAmbiguousProvider(url) {
|
|
208
|
+
return (`Could not auto-detect the QA provider at ${url}. ` +
|
|
209
|
+
`Set QA_MCP_PROVIDER to "walnut" or "simplifyqa" explicitly.`);
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live backend connection: URL + token supplied AT RUNTIME by the user.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. `resolveConfig()` reads credentials once, from the environment or
|
|
5
|
+
* `~/.common-qa-mcp/config.json`, and `createAdapter()` is called once at mount -- so connecting
|
|
6
|
+
* meant restarting the agent with env vars set. That is the wrong shape for the product: an AI
|
|
7
|
+
* client is supposed to connect by handing the MCP an app URL and a token, in conversation
|
|
8
|
+
* ("connect to https://… with this token"), and switch project or org without a restart.
|
|
9
|
+
*
|
|
10
|
+
* So the resolved config becomes the STARTING point and this holder owns what is current. Tools
|
|
11
|
+
* read `connection.adapter()` per call instead of closing over one built at mount; when there are
|
|
12
|
+
* no credentials it is `null` and the offline authoring path (record -> preview -> draft, no
|
|
13
|
+
* backend, no network) works exactly as before -- that mode is the whole verified surface and must
|
|
14
|
+
* never require a token.
|
|
15
|
+
*
|
|
16
|
+
* WHAT IT DELIBERATELY DOES NOT DO:
|
|
17
|
+
* - No credential on disk. The TOKEN lives in memory for the life of the process. Persisting it
|
|
18
|
+
* would put a credential in a file this feature has no mandate to manage; `~/.common-qa-mcp/
|
|
19
|
+
* config.json` already exists for users who WANT that, and it is still read at boot. (The
|
|
20
|
+
* last-selected projectId — a NON-secret id — IS remembered across restarts via projectStore,
|
|
21
|
+
* so a client relaunch doesn't drop you back to project-less; the token is never written.)
|
|
22
|
+
* - No logging of the token, ever -- `describe()` reports its presence and length, nothing else.
|
|
23
|
+
* - No optimistic accept: `connect()` VALIDATES against the backend and keeps the previous
|
|
24
|
+
* connection when validation fails, so a typo cannot silently break a working session.
|
|
25
|
+
*
|
|
26
|
+
* SCOPE: process-wide, not per MCP client. The agent is a single-user local process (loopback-bound,
|
|
27
|
+
* one desktop user), so one current connection is the honest model; two AI clients pointed at
|
|
28
|
+
* different orgs through the same agent would share it. Documented rather than pretended away.
|
|
29
|
+
*/
|
|
30
|
+
import type { ProviderAdapter } from './adapters/types.js';
|
|
31
|
+
import type { QaConfig } from './config.js';
|
|
32
|
+
export interface ConnectRequest {
|
|
33
|
+
/** Walnut app URL, e.g. `https://app.walnut.ai` or a self-hosted origin. */
|
|
34
|
+
url: string;
|
|
35
|
+
/** Bearer token: a Walnut session JWT, or an MCP API key. */
|
|
36
|
+
token: string;
|
|
37
|
+
/** Required for a JWT (the token alone carries no project context); optional for an API key,
|
|
38
|
+
* which is already project-scoped server-side. */
|
|
39
|
+
projectId?: string;
|
|
40
|
+
/** `jwt` (default) sends `Authorization` + `x-project-id`; `key` sends only `Authorization`.
|
|
41
|
+
* Mirrors WalnutAdapter.headers(). */
|
|
42
|
+
auth?: 'jwt' | 'key';
|
|
43
|
+
/** Which product to route to. `walnut` (default) uses the in-process Walnut adapter (REST);
|
|
44
|
+
* `simplifyqa` routes to the SQA agent MCP (a remote MCP) — `url` is then its /mcp endpoint. */
|
|
45
|
+
product?: 'walnut' | 'simplifyqa';
|
|
46
|
+
}
|
|
47
|
+
export interface ConnectionDescription {
|
|
48
|
+
mode: 'offline' | 'connected';
|
|
49
|
+
url?: string;
|
|
50
|
+
projectId?: string;
|
|
51
|
+
auth?: 'jwt' | 'key';
|
|
52
|
+
/** Presence + length only. The value is never logged or returned. */
|
|
53
|
+
token?: string;
|
|
54
|
+
/** Where the current credentials came from, so a user can tell a stale env var from what they
|
|
55
|
+
* just typed. */
|
|
56
|
+
source: 'none' | 'startup-config' | 'runtime-connect';
|
|
57
|
+
}
|
|
58
|
+
export declare class QaConnection {
|
|
59
|
+
private readonly startup;
|
|
60
|
+
private current;
|
|
61
|
+
private source;
|
|
62
|
+
private cached;
|
|
63
|
+
/** The config the adapter in `cached` was built from -- rebuild only when it actually changes. */
|
|
64
|
+
private cachedFor;
|
|
65
|
+
constructor(startup: QaConfig);
|
|
66
|
+
/** The adapter for the CURRENT credentials, or null when offline. Call per request. */
|
|
67
|
+
adapter(): ProviderAdapter | null;
|
|
68
|
+
config(): QaConfig;
|
|
69
|
+
describe(): ConnectionDescription;
|
|
70
|
+
/**
|
|
71
|
+
* Validate `req` against the backend and adopt it on success.
|
|
72
|
+
*
|
|
73
|
+
* Validation is `adapter.info()` -- `GET /api/projects/:id` -- which is the one call that proves
|
|
74
|
+
* all three parts at once: the URL is reachable, the token is accepted, and the project exists
|
|
75
|
+
* and is visible to that token. On failure NOTHING changes.
|
|
76
|
+
*/
|
|
77
|
+
connect(req: ConnectRequest): Promise<{
|
|
78
|
+
ok: true;
|
|
79
|
+
info: unknown;
|
|
80
|
+
} | {
|
|
81
|
+
ok: false;
|
|
82
|
+
error: string;
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Switch to a different project on the CURRENT connection -- same url/token/product/auth, only the
|
|
86
|
+
* projectId changes. Reuses connect()'s validate-before-adopt contract (probe.info()), so a bad id
|
|
87
|
+
* leaves the working connection untouched. Requires a connected session; offline callers are told
|
|
88
|
+
* to qa_connect first. Lets an AI client change project WITHOUT re-sending credentials.
|
|
89
|
+
*/
|
|
90
|
+
switchProject(projectId: string): Promise<{
|
|
91
|
+
ok: true;
|
|
92
|
+
info: unknown;
|
|
93
|
+
} | {
|
|
94
|
+
ok: false;
|
|
95
|
+
error: string;
|
|
96
|
+
}>;
|
|
97
|
+
/** Back to the credentials the process started with (usually none). Offline authoring is
|
|
98
|
+
* unaffected either way -- it never used a connection. */
|
|
99
|
+
disconnect(): void;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAqB,QAAQ,EAAE,MAAM,aAAa,CAAC;AAM/D,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd;uDACmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;2CACuC;IACvC,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACrB;qGACiG;IACjG,OAAO,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;CACnC;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,SAAS,GAAG,WAAW,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACrB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;sBACkB;IAClB,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,iBAAiB,CAAC;CACvD;AAED,qBAAa,YAAY;IAOX,OAAO,CAAC,QAAQ,CAAC,OAAO;IANpC,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,MAAM,CAAyB;IACvC,kGAAkG;IAClG,OAAO,CAAC,SAAS,CAAyB;gBAEb,OAAO,EAAE,QAAQ;IAoB9C,uFAAuF;IACvF,OAAO,IAAI,eAAe,GAAG,IAAI;IAOjC,MAAM,IAAI,QAAQ;IAIlB,QAAQ,IAAI,qBAAqB;IAajC;;;;;;OAMG;IACG,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAwDvG;;;;;OAKG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IA2B3G;+DAC2D;IAC3D,UAAU,IAAI,IAAI;CAOnB"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live backend connection: URL + token supplied AT RUNTIME by the user.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. `resolveConfig()` reads credentials once, from the environment or
|
|
5
|
+
* `~/.common-qa-mcp/config.json`, and `createAdapter()` is called once at mount -- so connecting
|
|
6
|
+
* meant restarting the agent with env vars set. That is the wrong shape for the product: an AI
|
|
7
|
+
* client is supposed to connect by handing the MCP an app URL and a token, in conversation
|
|
8
|
+
* ("connect to https://… with this token"), and switch project or org without a restart.
|
|
9
|
+
*
|
|
10
|
+
* So the resolved config becomes the STARTING point and this holder owns what is current. Tools
|
|
11
|
+
* read `connection.adapter()` per call instead of closing over one built at mount; when there are
|
|
12
|
+
* no credentials it is `null` and the offline authoring path (record -> preview -> draft, no
|
|
13
|
+
* backend, no network) works exactly as before -- that mode is the whole verified surface and must
|
|
14
|
+
* never require a token.
|
|
15
|
+
*
|
|
16
|
+
* WHAT IT DELIBERATELY DOES NOT DO:
|
|
17
|
+
* - No credential on disk. The TOKEN lives in memory for the life of the process. Persisting it
|
|
18
|
+
* would put a credential in a file this feature has no mandate to manage; `~/.common-qa-mcp/
|
|
19
|
+
* config.json` already exists for users who WANT that, and it is still read at boot. (The
|
|
20
|
+
* last-selected projectId — a NON-secret id — IS remembered across restarts via projectStore,
|
|
21
|
+
* so a client relaunch doesn't drop you back to project-less; the token is never written.)
|
|
22
|
+
* - No logging of the token, ever -- `describe()` reports its presence and length, nothing else.
|
|
23
|
+
* - No optimistic accept: `connect()` VALIDATES against the backend and keeps the previous
|
|
24
|
+
* connection when validation fails, so a typo cannot silently break a working session.
|
|
25
|
+
*
|
|
26
|
+
* SCOPE: process-wide, not per MCP client. The agent is a single-user local process (loopback-bound,
|
|
27
|
+
* one desktop user), so one current connection is the honest model; two AI clients pointed at
|
|
28
|
+
* different orgs through the same agent would share it. Documented rather than pretended away.
|
|
29
|
+
*/
|
|
30
|
+
import { createAdapter } from './adapters/factory.js';
|
|
31
|
+
import { readLastProject, writeLastProject } from './projectStore.js';
|
|
32
|
+
import { createLogger } from './vendor/logger.js';
|
|
33
|
+
const logger = createLogger('QaConnection');
|
|
34
|
+
export class QaConnection {
|
|
35
|
+
constructor(startup) {
|
|
36
|
+
this.startup = startup;
|
|
37
|
+
/** The config the adapter in `cached` was built from -- rebuild only when it actually changes. */
|
|
38
|
+
this.cachedFor = null;
|
|
39
|
+
this.current = startup;
|
|
40
|
+
this.source = startup.mode === 'connected' ? 'startup-config' : 'none';
|
|
41
|
+
this.cached = null;
|
|
42
|
+
// Restore the last-selected project across restarts. The startup config carries url + token
|
|
43
|
+
// but usually NO project, so a client relaunch otherwise drops the connection back to
|
|
44
|
+
// project-less — which breaks downstream per-project model resolution for AI recording. If we
|
|
45
|
+
// booted connected to a backend we have a remembered project for, adopt it. Optimistic (no
|
|
46
|
+
// async validate here): the id was validated when it was saved via switchProject/connect; if it
|
|
47
|
+
// is since stale, project-scoped calls fail cleanly and the user can switch — the connection
|
|
48
|
+
// itself is fine.
|
|
49
|
+
if (startup.mode === 'connected' && !startup.projectId) {
|
|
50
|
+
const remembered = readLastProject(startup.url);
|
|
51
|
+
if (remembered) {
|
|
52
|
+
this.current = { ...startup, projectId: remembered };
|
|
53
|
+
logger.info(`Restored last-selected project ${remembered} for ${startup.url}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** The adapter for the CURRENT credentials, or null when offline. Call per request. */
|
|
58
|
+
adapter() {
|
|
59
|
+
if (this.cachedFor === this.current)
|
|
60
|
+
return this.cached;
|
|
61
|
+
this.cached = createAdapter(this.current);
|
|
62
|
+
this.cachedFor = this.current;
|
|
63
|
+
return this.cached;
|
|
64
|
+
}
|
|
65
|
+
config() {
|
|
66
|
+
return this.current;
|
|
67
|
+
}
|
|
68
|
+
describe() {
|
|
69
|
+
const c = this.current;
|
|
70
|
+
if (c.mode !== 'connected')
|
|
71
|
+
return { mode: 'offline', source: this.source };
|
|
72
|
+
return {
|
|
73
|
+
mode: 'connected',
|
|
74
|
+
url: c.url,
|
|
75
|
+
projectId: c.projectId ?? '', // '' = connected but no project selected yet
|
|
76
|
+
auth: c.walnutAuth === 'key' ? 'key' : 'jwt',
|
|
77
|
+
token: `present (${c.token.length} chars)`,
|
|
78
|
+
source: this.source,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Validate `req` against the backend and adopt it on success.
|
|
83
|
+
*
|
|
84
|
+
* Validation is `adapter.info()` -- `GET /api/projects/:id` -- which is the one call that proves
|
|
85
|
+
* all three parts at once: the URL is reachable, the token is accepted, and the project exists
|
|
86
|
+
* and is visible to that token. On failure NOTHING changes.
|
|
87
|
+
*/
|
|
88
|
+
async connect(req) {
|
|
89
|
+
const url = req.url?.trim();
|
|
90
|
+
// Tolerate a pasted/`headers.Authorization`-style "Bearer <token>": the field is labelled
|
|
91
|
+
// "Bearer token" and MCP-client header values carry the scheme, but we add `Bearer ` ourselves
|
|
92
|
+
// downstream -- so keep only the raw credential and never send "Bearer Bearer ...".
|
|
93
|
+
const token = (req.token ?? '').trim().replace(/^Bearer\s+/i, '').trim();
|
|
94
|
+
if (!url || !token)
|
|
95
|
+
return { ok: false, error: 'url and token are both required.' };
|
|
96
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
97
|
+
return { ok: false, error: `url must be absolute and start with http:// or https:// -- got "${url}".` };
|
|
98
|
+
}
|
|
99
|
+
const product = req.product === 'simplifyqa' ? 'simplifyqa' : 'walnut';
|
|
100
|
+
const auth = req.auth === 'key' ? 'key' : 'jwt';
|
|
101
|
+
const projectId = req.projectId?.trim() ?? '';
|
|
102
|
+
// The JWT-needs-projectId rule is Walnut-specific (it sends x-project-id). SQA forwards the token
|
|
103
|
+
// to its own MCP, which resolves tenant context however it likes, so we don't force it there.
|
|
104
|
+
if (product === 'walnut' && auth === 'jwt' && !projectId) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: 'projectId is required with a JWT (the token alone carries no project context). '
|
|
108
|
+
+ 'Pass projectId, or auth:"key" if this is an MCP API key.',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const candidate = {
|
|
112
|
+
...this.startup,
|
|
113
|
+
mode: 'connected',
|
|
114
|
+
provider: product,
|
|
115
|
+
url: url.replace(/\/+$/, ''),
|
|
116
|
+
token,
|
|
117
|
+
// An API key is project-scoped server-side; keep whatever the startup config had so
|
|
118
|
+
// project-scoped helpers still have something to name, but never require it.
|
|
119
|
+
projectId: projectId || (this.startup.projectId ?? ''),
|
|
120
|
+
walnutAuth: auth,
|
|
121
|
+
};
|
|
122
|
+
const probe = createAdapter(candidate);
|
|
123
|
+
if (!probe)
|
|
124
|
+
return { ok: false, error: `Could not build an adapter for product "${product}".` };
|
|
125
|
+
try {
|
|
126
|
+
const info = await probe.info();
|
|
127
|
+
this.current = candidate;
|
|
128
|
+
this.cached = probe;
|
|
129
|
+
this.cachedFor = candidate;
|
|
130
|
+
this.source = 'runtime-connect';
|
|
131
|
+
// Remember the project so a later restart reboots into it (see projectStore). Only when one
|
|
132
|
+
// was actually chosen — a key-scoped connect may carry none.
|
|
133
|
+
if (candidate.projectId)
|
|
134
|
+
writeLastProject(candidate.url, candidate.projectId);
|
|
135
|
+
// URL and project only -- never the token, not even truncated.
|
|
136
|
+
logger.info(`Connected to ${candidate.url} (project ${candidate.projectId || '(key-scoped)'}, auth ${auth})`);
|
|
137
|
+
return { ok: true, info };
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
+
logger.warn(`connect rejected for ${url}: ${message}`);
|
|
142
|
+
return { ok: false, error: message };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Switch to a different project on the CURRENT connection -- same url/token/product/auth, only the
|
|
147
|
+
* projectId changes. Reuses connect()'s validate-before-adopt contract (probe.info()), so a bad id
|
|
148
|
+
* leaves the working connection untouched. Requires a connected session; offline callers are told
|
|
149
|
+
* to qa_connect first. Lets an AI client change project WITHOUT re-sending credentials.
|
|
150
|
+
*/
|
|
151
|
+
async switchProject(projectId) {
|
|
152
|
+
const pid = projectId?.trim();
|
|
153
|
+
if (!pid)
|
|
154
|
+
return { ok: false, error: 'projectId is required.' };
|
|
155
|
+
const c = this.current;
|
|
156
|
+
if (c.mode !== 'connected') {
|
|
157
|
+
return { ok: false, error: 'Not connected — use qa_connect first, then switch project.' };
|
|
158
|
+
}
|
|
159
|
+
const candidate = { ...c, projectId: pid };
|
|
160
|
+
const probe = createAdapter(candidate);
|
|
161
|
+
if (!probe)
|
|
162
|
+
return { ok: false, error: `Could not build an adapter for project "${pid}".` };
|
|
163
|
+
try {
|
|
164
|
+
const info = await probe.info();
|
|
165
|
+
this.current = candidate;
|
|
166
|
+
this.cached = probe;
|
|
167
|
+
this.cachedFor = candidate;
|
|
168
|
+
this.source = 'runtime-connect';
|
|
169
|
+
// Remember it so a restart reboots into this project instead of project-less (projectStore).
|
|
170
|
+
writeLastProject(candidate.url, pid);
|
|
171
|
+
logger.info(`Switched to project ${pid}`);
|
|
172
|
+
return { ok: true, info };
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
176
|
+
logger.warn(`switchProject rejected for ${pid}: ${message}`);
|
|
177
|
+
return { ok: false, error: message };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Back to the credentials the process started with (usually none). Offline authoring is
|
|
181
|
+
* unaffected either way -- it never used a connection. */
|
|
182
|
+
disconnect() {
|
|
183
|
+
this.current = this.startup;
|
|
184
|
+
this.source = this.startup.mode === 'connected' ? 'startup-config' : 'none';
|
|
185
|
+
this.cached = null;
|
|
186
|
+
this.cachedFor = null;
|
|
187
|
+
logger.info('Disconnected — back to the startup configuration');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"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"}
|