@askthew/mcp-plugin 0.4.0 → 0.4.2
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/README.md +24 -13
- package/dist/auth-pending.test.d.ts +1 -0
- package/dist/auth-pending.test.js +56 -0
- package/dist/cli-actions.test.d.ts +1 -0
- package/dist/cli-actions.test.js +71 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +293 -37
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +274 -0
- package/dist/free-tier-policy.test.d.ts +1 -0
- package/dist/free-tier-policy.test.js +57 -0
- package/dist/index.d.ts +47 -13
- package/dist/index.js +1103 -106
- package/dist/index.test.js +609 -6
- package/dist/install.d.ts +40 -0
- package/dist/install.js +155 -18
- package/dist/install.test.js +62 -2
- package/dist/lib/auth-pending.d.ts +23 -0
- package/dist/lib/auth-pending.js +36 -0
- package/dist/lib/cli-actions.d.ts +28 -0
- package/dist/lib/cli-actions.js +104 -0
- package/dist/lib/free-install-registration.d.ts +27 -0
- package/dist/lib/free-install-registration.js +52 -0
- package/dist/lib/free-tier-policy.d.ts +5 -1
- package/dist/lib/free-tier-policy.js +16 -1
- package/dist/lib/local-identity.d.ts +44 -0
- package/dist/lib/local-identity.js +81 -0
- package/dist/lib/local-store.d.ts +33 -2
- package/dist/lib/local-store.js +191 -19
- package/dist/lib/paths.d.ts +2 -0
- package/dist/lib/paths.js +6 -0
- package/dist/lib/telemetry.js +28 -2
- package/dist/lib/timeline-insights.d.ts +23 -0
- package/dist/lib/timeline-insights.js +115 -0
- package/dist/lib/upgrade-nudge.d.ts +1 -1
- package/dist/lib/upgrade-nudge.js +8 -1
- package/dist/local-identity.test.d.ts +1 -0
- package/dist/local-identity.test.js +29 -0
- package/dist/local-store.test.js +34 -0
- package/dist/scope.d.ts +1 -1
- package/dist/scope.js +56 -2
- package/dist/scope.test.js +17 -0
- package/dist/timeline-insights.test.d.ts +1 -0
- package/dist/timeline-insights.test.js +85 -0
- package/package.json +2 -2
package/dist/index.test.js
CHANGED
|
@@ -1,8 +1,113 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { codingSessionSignalSchema, createAskTheWMcpServer, normalizeInstallTokenInput, redactCodingSessionSignal, redactProvenanceSignal, } from "./index.js";
|
|
7
|
+
import { LocalStore } from "./lib/local-store.js";
|
|
8
|
+
import { credentialsPath, writePrivateJson } from "./lib/paths.js";
|
|
9
|
+
function toolResultJson(result) {
|
|
10
|
+
return JSON.parse(result.content[0].text);
|
|
11
|
+
}
|
|
12
|
+
async function withFreeEnv(fn) {
|
|
13
|
+
const previous = {
|
|
14
|
+
ASKTHEW_CLI_TOKEN: process.env.ASKTHEW_CLI_TOKEN,
|
|
15
|
+
ASKTHEW_USER_ID: process.env.ASKTHEW_USER_ID,
|
|
16
|
+
ASKTHEW_CLI_TOKEN_ID: process.env.ASKTHEW_CLI_TOKEN_ID,
|
|
17
|
+
ASKTHEW_DATA_DIR: process.env.ASKTHEW_DATA_DIR,
|
|
18
|
+
ASKTHEW_INSTALL_TOKEN: process.env.ASKTHEW_INSTALL_TOKEN,
|
|
19
|
+
ASKTHEW_FREE_MODE: process.env.ASKTHEW_FREE_MODE,
|
|
20
|
+
};
|
|
21
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-free-tools-"));
|
|
22
|
+
process.env.ASKTHEW_CLI_TOKEN = "cli_free_token";
|
|
23
|
+
process.env.ASKTHEW_USER_ID = "local-user";
|
|
24
|
+
process.env.ASKTHEW_CLI_TOKEN_ID = "cli-token-id";
|
|
25
|
+
process.env.ASKTHEW_DATA_DIR = dataDir;
|
|
26
|
+
delete process.env.ASKTHEW_INSTALL_TOKEN;
|
|
27
|
+
delete process.env.ASKTHEW_FREE_MODE;
|
|
28
|
+
try {
|
|
29
|
+
return await fn();
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
for (const [key, value] of Object.entries(previous)) {
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
delete process.env[key];
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
process.env[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function withPendingFreeEnv(fn) {
|
|
44
|
+
const previous = {
|
|
45
|
+
ASKTHEW_CLI_TOKEN: process.env.ASKTHEW_CLI_TOKEN,
|
|
46
|
+
ASKTHEW_USER_ID: process.env.ASKTHEW_USER_ID,
|
|
47
|
+
ASKTHEW_CLI_TOKEN_ID: process.env.ASKTHEW_CLI_TOKEN_ID,
|
|
48
|
+
ASKTHEW_DATA_DIR: process.env.ASKTHEW_DATA_DIR,
|
|
49
|
+
ASKTHEW_INSTALL_TOKEN: process.env.ASKTHEW_INSTALL_TOKEN,
|
|
50
|
+
ASKTHEW_FREE_MODE: process.env.ASKTHEW_FREE_MODE,
|
|
51
|
+
};
|
|
52
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-pending-free-tools-"));
|
|
53
|
+
process.env.ASKTHEW_FREE_MODE = "1";
|
|
54
|
+
process.env.ASKTHEW_DATA_DIR = dataDir;
|
|
55
|
+
delete process.env.ASKTHEW_CLI_TOKEN;
|
|
56
|
+
delete process.env.ASKTHEW_USER_ID;
|
|
57
|
+
delete process.env.ASKTHEW_CLI_TOKEN_ID;
|
|
58
|
+
delete process.env.ASKTHEW_INSTALL_TOKEN;
|
|
59
|
+
try {
|
|
60
|
+
return await fn();
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
for (const [key, value] of Object.entries(previous)) {
|
|
64
|
+
if (value === undefined) {
|
|
65
|
+
delete process.env[key];
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
process.env[key] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function withInstalledFreeEnv(fn) {
|
|
75
|
+
const previous = {
|
|
76
|
+
ASKTHEW_CLI_TOKEN: process.env.ASKTHEW_CLI_TOKEN,
|
|
77
|
+
ASKTHEW_USER_ID: process.env.ASKTHEW_USER_ID,
|
|
78
|
+
ASKTHEW_CLI_TOKEN_ID: process.env.ASKTHEW_CLI_TOKEN_ID,
|
|
79
|
+
ASKTHEW_DATA_DIR: process.env.ASKTHEW_DATA_DIR,
|
|
80
|
+
ASKTHEW_INSTALL_TOKEN: process.env.ASKTHEW_INSTALL_TOKEN,
|
|
81
|
+
ASKTHEW_FREE_MODE: process.env.ASKTHEW_FREE_MODE,
|
|
82
|
+
};
|
|
83
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-installed-free-tools-"));
|
|
84
|
+
process.env.ASKTHEW_FREE_MODE = "1";
|
|
85
|
+
process.env.ASKTHEW_DATA_DIR = dataDir;
|
|
86
|
+
delete process.env.ASKTHEW_CLI_TOKEN;
|
|
87
|
+
delete process.env.ASKTHEW_USER_ID;
|
|
88
|
+
delete process.env.ASKTHEW_CLI_TOKEN_ID;
|
|
89
|
+
delete process.env.ASKTHEW_INSTALL_TOKEN;
|
|
90
|
+
writePrivateJson(credentialsPath(), {
|
|
91
|
+
email: "ymtest89+test5@gmail.com",
|
|
92
|
+
userId: "local-user",
|
|
93
|
+
cliToken: "cli_free_token",
|
|
94
|
+
cliTokenId: "cli-token-id",
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
return await fn();
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
for (const [key, value] of Object.entries(previous)) {
|
|
101
|
+
if (value === undefined) {
|
|
102
|
+
delete process.env[key];
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
process.env[key] = value;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
6
111
|
test("install token normalization accepts copied shell-quoted values", () => {
|
|
7
112
|
assert.equal(normalizeInstallTokenInput("'atw_mcp_token'"), "atw_mcp_token");
|
|
8
113
|
assert.equal(normalizeInstallTokenInput('"atw_mcp_token"'), "atw_mcp_token");
|
|
@@ -38,6 +143,19 @@ test("coding session signal redaction removes obvious secrets", () => {
|
|
|
38
143
|
assert.match(redacted.commandsRun[0] ?? "", /\[REDACTED\]/);
|
|
39
144
|
assert.deepEqual(redacted.metadata, { nested: { token: "[REDACTED]" } });
|
|
40
145
|
});
|
|
146
|
+
test("capture redactor catches documented capture-path patterns", () => {
|
|
147
|
+
const redacted = redactCodingSessionSignal({
|
|
148
|
+
sessionId: "session-1",
|
|
149
|
+
sequence: 11,
|
|
150
|
+
kind: "session_checkpoint",
|
|
151
|
+
summary: "OPENAI_API_KEY=sk-proj_abcdefghijklmnopqrstuvwxyz123456 Bearer abc.def-ghi user@example.com AKIA1234567890ABCDEF eyJabcdefghijklmnopqrstuv.abcdefghijklmnopqrstuv.abcdefghijklmnopqrstuv",
|
|
152
|
+
evidence: [],
|
|
153
|
+
filesTouched: [],
|
|
154
|
+
commandsRun: [],
|
|
155
|
+
metadata: {},
|
|
156
|
+
});
|
|
157
|
+
assert.equal(redacted.summary, "[REDACTED] [REDACTED] [REDACTED] [REDACTED] [REDACTED]");
|
|
158
|
+
});
|
|
41
159
|
test("redacts ATW hyphen-segmented install tokens in commands", () => {
|
|
42
160
|
const redacted = redactCodingSessionSignal({
|
|
43
161
|
sessionId: "session-1",
|
|
@@ -168,6 +286,13 @@ test("happy-path MCP source exposes capture_session_signal and v1 API tools", ()
|
|
|
168
286
|
"create_decision",
|
|
169
287
|
"update_decision",
|
|
170
288
|
"delete_decision",
|
|
289
|
+
"review_decisions",
|
|
290
|
+
"review_session",
|
|
291
|
+
"recap",
|
|
292
|
+
"coach",
|
|
293
|
+
"promote_signal_to_decision",
|
|
294
|
+
"list_decision_candidates",
|
|
295
|
+
"search_trail",
|
|
171
296
|
"list_outcomes",
|
|
172
297
|
"get_outcome",
|
|
173
298
|
"list_outcome_signals",
|
|
@@ -178,6 +303,7 @@ test("happy-path MCP source exposes capture_session_signal and v1 API tools", ()
|
|
|
178
303
|
"update_north_star",
|
|
179
304
|
"list_signals",
|
|
180
305
|
"get_signal",
|
|
306
|
+
"find_signal_by_summary",
|
|
181
307
|
]) {
|
|
182
308
|
assert.match(source, new RegExp(`"${toolName}"`));
|
|
183
309
|
}
|
|
@@ -187,6 +313,65 @@ test("happy-path MCP source exposes capture_session_signal and v1 API tools", ()
|
|
|
187
313
|
assert.doesNotMatch(source, /server\.tool\(\s*"get_session_decisions"/);
|
|
188
314
|
assert.doesNotMatch(source, /server\.tool\(\s*"link_outcome"/);
|
|
189
315
|
assert.doesNotMatch(source, /server\.tool\(\s*"get_decision_feed"/);
|
|
316
|
+
assert.doesNotMatch(source, /server\.tool\(\s*"analyze_session"/);
|
|
317
|
+
});
|
|
318
|
+
test("schema-handler contract registers every documented MCP tool", async () => {
|
|
319
|
+
await withFreeEnv(async () => {
|
|
320
|
+
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
321
|
+
const tools = server._registeredTools;
|
|
322
|
+
const expected = [
|
|
323
|
+
"capture_session_signal",
|
|
324
|
+
"list_signals",
|
|
325
|
+
"get_signal",
|
|
326
|
+
"find_signal_by_summary",
|
|
327
|
+
"list_decisions",
|
|
328
|
+
"get_decision",
|
|
329
|
+
"create_decision",
|
|
330
|
+
"update_decision",
|
|
331
|
+
"delete_decision",
|
|
332
|
+
"review_decisions",
|
|
333
|
+
"review_session",
|
|
334
|
+
"view_timeline",
|
|
335
|
+
"recap",
|
|
336
|
+
"coach",
|
|
337
|
+
"promote_signal_to_decision",
|
|
338
|
+
"list_decision_candidates",
|
|
339
|
+
"search_trail",
|
|
340
|
+
"list_outcomes",
|
|
341
|
+
"get_outcome",
|
|
342
|
+
"list_outcome_signals",
|
|
343
|
+
"create_outcome",
|
|
344
|
+
"update_outcome",
|
|
345
|
+
"delete_outcome",
|
|
346
|
+
"get_north_star",
|
|
347
|
+
"update_north_star",
|
|
348
|
+
"export_decisions",
|
|
349
|
+
];
|
|
350
|
+
for (const toolName of expected) {
|
|
351
|
+
assert.equal(typeof tools[toolName]?.handler, "function", `${toolName} handler`);
|
|
352
|
+
assert.ok(tools[toolName]?.inputSchema, `${toolName} schema`);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
test("every write tool accepts an idempotency key parameter", () => {
|
|
357
|
+
const source = fs.readFileSync(path.resolve(process.cwd(), "src/index.ts"), "utf8");
|
|
358
|
+
for (const toolName of [
|
|
359
|
+
"capture_session_signal",
|
|
360
|
+
"create_decision",
|
|
361
|
+
"update_decision",
|
|
362
|
+
"delete_decision",
|
|
363
|
+
"create_outcome",
|
|
364
|
+
"update_outcome",
|
|
365
|
+
"delete_outcome",
|
|
366
|
+
"update_north_star",
|
|
367
|
+
"promote_signal_to_decision",
|
|
368
|
+
]) {
|
|
369
|
+
const start = source.indexOf(`"${toolName}"`);
|
|
370
|
+
assert.notEqual(start, -1, `${toolName} registered`);
|
|
371
|
+
const nextTool = source.indexOf("server.tool(", start + 1);
|
|
372
|
+
const block = source.slice(start, nextTool === -1 ? undefined : nextTool);
|
|
373
|
+
assert.match(block, /idempotencyKey/, `${toolName} idempotencyKey`);
|
|
374
|
+
}
|
|
190
375
|
});
|
|
191
376
|
test("v1 API MCP tools dispatch to expected HTTP routes with install-token auth", async () => {
|
|
192
377
|
const calls = [];
|
|
@@ -213,21 +398,22 @@ test("v1 API MCP tools dispatch to expected HTTP routes with install-token auth"
|
|
|
213
398
|
});
|
|
214
399
|
const tools = server._registeredTools;
|
|
215
400
|
const cases = [
|
|
216
|
-
{ name: "list_decisions", payload: { limit: 5, cursor: "c1" }, method: "GET", path: "/api/decisions?limit=5&cursor=c1" },
|
|
401
|
+
{ name: "list_decisions", payload: { limit: 5, cursor: "c1" }, method: "GET", path: "/api/decisions?limit=5&cursor=c1&compact=true&max_chars=8000" },
|
|
217
402
|
{ name: "get_decision", payload: { id: "d1" }, method: "GET", path: "/api/decisions/d1" },
|
|
218
|
-
{ name: "create_decision", payload: { content: "Adopt Bun" }, method: "POST", path: "/api/decisions" },
|
|
219
|
-
{ name: "update_decision", payload: { id: "d1", headline: "Adopt Bun v2" }, method: "PATCH", path: "/api/decisions/d1" },
|
|
403
|
+
{ name: "create_decision", payload: { content: "Adopt Bun", idempotencyKey: "idem-create" }, method: "POST", path: "/api/decisions?response_shape=v2" },
|
|
404
|
+
{ name: "update_decision", payload: { id: "d1", headline: "Adopt Bun v2", idempotencyKey: "idem-update" }, method: "PATCH", path: "/api/decisions/d1?response_shape=v2" },
|
|
220
405
|
{ name: "delete_decision", payload: { id: "d1", confirmText: "Adopt Bun v2" }, method: "DELETE", path: "/api/decisions/d1" },
|
|
221
406
|
{ name: "list_outcomes", payload: { limit: 10 }, method: "GET", path: "/api/outcomes?limit=10" },
|
|
222
407
|
{ name: "get_outcome", payload: { id: "o1" }, method: "GET", path: "/api/outcomes/o1" },
|
|
223
408
|
{ name: "list_outcome_signals", payload: { id: "o1" }, method: "GET", path: "/api/outcomes/o1/signals" },
|
|
224
409
|
{ name: "create_outcome", payload: { name: "Reduce churn" }, method: "POST", path: "/api/outcomes" },
|
|
225
|
-
{ name: "update_outcome", payload: { id: "o1", summary: "New summary" }, method: "PATCH", path: "/api/outcomes/o1" },
|
|
410
|
+
{ name: "update_outcome", payload: { id: "o1", summary: "New summary", idempotencyKey: "idem-outcome" }, method: "PATCH", path: "/api/outcomes/o1?response_shape=v2" },
|
|
226
411
|
{ name: "delete_outcome", payload: { id: "o1", confirmText: "Reduce churn" }, method: "DELETE", path: "/api/outcomes/o1" },
|
|
227
412
|
{ name: "get_north_star", payload: {}, method: "GET", path: "/api/north-star" },
|
|
228
413
|
{ name: "update_north_star", payload: { metric: "Active users", current: "10", target: "100", reason: "API smoke" }, method: "POST", path: "/api/north-star" },
|
|
229
|
-
{ name: "list_signals", payload: { limit: 25, cursor: "2026-01-01T00:00:00.000Z" }, method: "GET", path: "/api/signals?limit=25&cursor=2026-01-01T00%3A00%3A00.000Z" },
|
|
414
|
+
{ name: "list_signals", payload: { limit: 25, cursor: "2026-01-01T00:00:00.000Z" }, method: "GET", path: "/api/signals?limit=25&cursor=2026-01-01T00%3A00%3A00.000Z&compact=true&max_chars=8000" },
|
|
230
415
|
{ name: "get_signal", payload: { id: "s1" }, method: "GET", path: "/api/signals/s1" },
|
|
416
|
+
{ name: "find_signal_by_summary", payload: { query: "adopt", limit: 5 }, method: "GET", path: "/api/signals?query=adopt&limit=5&compact=true&max_chars=8000" },
|
|
231
417
|
];
|
|
232
418
|
for (const entry of cases) {
|
|
233
419
|
assert.ok(tools[entry.name], `${entry.name} should be registered`);
|
|
@@ -240,6 +426,7 @@ test("v1 API MCP tools dispatch to expected HTTP routes with install-token auth"
|
|
|
240
426
|
assert.equal(call.url, `https://askthew.example.com${entry.path}`);
|
|
241
427
|
assert.deepEqual(call.headers, {
|
|
242
428
|
...(entry.method === "GET" ? {} : { "Content-Type": "application/json" }),
|
|
429
|
+
...(entry.payload.idempotencyKey ? { "Idempotency-Key": entry.payload.idempotencyKey } : {}),
|
|
243
430
|
Authorization: "Bearer atw_mcp_tools",
|
|
244
431
|
});
|
|
245
432
|
if (entry.method !== "GET") {
|
|
@@ -266,6 +453,25 @@ test("v1 API MCP tools return server errors as JSON text", async () => {
|
|
|
266
453
|
assert.equal(parsed.status, 500);
|
|
267
454
|
assert.equal(parsed.code, "nope");
|
|
268
455
|
});
|
|
456
|
+
test("compact write tools relay upstream errors instead of pretending success", async () => {
|
|
457
|
+
const server = createAskTheWMcpServer({
|
|
458
|
+
apiBaseUrl: "https://askthew.example.com",
|
|
459
|
+
sendStartupHeartbeat: false,
|
|
460
|
+
credentials: {
|
|
461
|
+
installToken: "atw_mcp_tools",
|
|
462
|
+
clientId: "codex",
|
|
463
|
+
},
|
|
464
|
+
fetchImpl: (async () => new Response(JSON.stringify({ ok: false, code: "invalid_input", field: "content", hint: "Provide content." }), {
|
|
465
|
+
status: 422,
|
|
466
|
+
})),
|
|
467
|
+
});
|
|
468
|
+
const result = await server._registeredTools.create_decision.handler({ content: " " });
|
|
469
|
+
const parsed = toolResultJson(result);
|
|
470
|
+
assert.equal(parsed.ok, false);
|
|
471
|
+
assert.equal(parsed.status, 422);
|
|
472
|
+
assert.equal(parsed.code, "invalid_input");
|
|
473
|
+
assert.equal(parsed.field, "content");
|
|
474
|
+
});
|
|
269
475
|
test("createAskTheWMcpServer sends startup heartbeat and automated setup signal", async () => {
|
|
270
476
|
const calls = [];
|
|
271
477
|
createAskTheWMcpServer({
|
|
@@ -341,9 +547,406 @@ test("createAskTheWMcpServer accepts hosted connector identity overrides", async
|
|
|
341
547
|
commandsRun: [],
|
|
342
548
|
metadata: {},
|
|
343
549
|
});
|
|
344
|
-
assert.equal(calls[0]?.url, "https://askthew.example.com/api/ingest/mcp");
|
|
550
|
+
assert.equal(calls[0]?.url, "https://askthew.example.com/api/ingest/mcp?response_shape=v2");
|
|
345
551
|
assert.equal(calls[0]?.body.installToken, "atw_mcp_remote");
|
|
346
552
|
assert.equal(calls[0]?.body.clientId, "claude_remote");
|
|
347
553
|
assert.equal(calls[0]?.body.clientLabel, "Claude Desktop / Cowork");
|
|
348
554
|
assert.equal(calls[0]?.body.sessionSignal.metadata.connector_mode, "remote_mcp");
|
|
349
555
|
});
|
|
556
|
+
test("free capture defaults to compact response and echo full returns full local payload", async () => {
|
|
557
|
+
await withFreeEnv(async () => {
|
|
558
|
+
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
559
|
+
const capture = server._registeredTools.capture_session_signal;
|
|
560
|
+
const basePayload = {
|
|
561
|
+
sessionId: "session-compact",
|
|
562
|
+
sequence: 1,
|
|
563
|
+
kind: "implementation_update",
|
|
564
|
+
summary: "Updated compact response shape.",
|
|
565
|
+
evidence: [],
|
|
566
|
+
filesTouched: ["src/index.ts"],
|
|
567
|
+
commandsRun: ["npm test"],
|
|
568
|
+
metadata: {},
|
|
569
|
+
};
|
|
570
|
+
const compact = toolResultJson(await capture.handler(basePayload));
|
|
571
|
+
assert.equal(compact.ok, true);
|
|
572
|
+
assert.equal(compact.sessionId, "session-compact");
|
|
573
|
+
assert.equal(compact.sequence, 1);
|
|
574
|
+
assert.equal(typeof compact.id, "number");
|
|
575
|
+
assert.ok(JSON.stringify(compact).length < 1024);
|
|
576
|
+
assert.equal("signal" in compact, false);
|
|
577
|
+
const full = toolResultJson(await capture.handler({ ...basePayload, sequence: 2, echo: "full" }));
|
|
578
|
+
assert.equal(full.ok, true);
|
|
579
|
+
assert.equal(full.signal.summary, "Updated compact response shape.");
|
|
580
|
+
assert.equal(full.signal.sessionId, "session-compact");
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
test("stale free install without identity self-heals into local capture without hosted workspace calls", async () => {
|
|
584
|
+
await withPendingFreeEnv(async () => {
|
|
585
|
+
const calls = [];
|
|
586
|
+
const server = createAskTheWMcpServer({
|
|
587
|
+
apiBaseUrl: "https://askthew.example.com",
|
|
588
|
+
fetchImpl: async (url) => {
|
|
589
|
+
calls.push({ url: String(url) });
|
|
590
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
594
|
+
const tools = server._registeredTools;
|
|
595
|
+
const capture = toolResultJson(await tools.capture_session_signal.handler({
|
|
596
|
+
sessionId: "session-pending",
|
|
597
|
+
sequence: 1,
|
|
598
|
+
kind: "setup_complete",
|
|
599
|
+
summary: "Stale free install should capture locally.",
|
|
600
|
+
evidence: [],
|
|
601
|
+
filesTouched: [],
|
|
602
|
+
commandsRun: [],
|
|
603
|
+
metadata: {},
|
|
604
|
+
}));
|
|
605
|
+
assert.equal(capture.ok, true);
|
|
606
|
+
assert.equal(capture.id, 1);
|
|
607
|
+
assert.equal(calls.length, 1);
|
|
608
|
+
assert.match(calls[0].url, /\/api\/cli\/v1\/free-installs\/register$/);
|
|
609
|
+
const store = LocalStore.open();
|
|
610
|
+
try {
|
|
611
|
+
const stats = store.stats();
|
|
612
|
+
assert.equal(stats.signals, 1);
|
|
613
|
+
assert.equal(stats.decisions, 0);
|
|
614
|
+
}
|
|
615
|
+
finally {
|
|
616
|
+
store.close();
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
test("authenticated free mode keeps capture, decisions, and review local without hosted calls", async () => {
|
|
621
|
+
await withFreeEnv(async () => {
|
|
622
|
+
const calls = [];
|
|
623
|
+
const server = createAskTheWMcpServer({
|
|
624
|
+
apiBaseUrl: "https://askthew.example.com",
|
|
625
|
+
fetchImpl: async (url) => {
|
|
626
|
+
calls.push({ url: String(url) });
|
|
627
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
631
|
+
const tools = server._registeredTools;
|
|
632
|
+
const capture = toolResultJson(await tools.capture_session_signal.handler({
|
|
633
|
+
sessionId: "session-local-only",
|
|
634
|
+
sequence: 1,
|
|
635
|
+
kind: "implementation_update",
|
|
636
|
+
summary: "Captured locally after free auth.",
|
|
637
|
+
evidence: [],
|
|
638
|
+
filesTouched: ["packages/mcp-plugin/src/index.ts"],
|
|
639
|
+
commandsRun: [],
|
|
640
|
+
metadata: {},
|
|
641
|
+
}));
|
|
642
|
+
const decision = toolResultJson(await tools.create_decision.handler({ content: "Keep free mode local-only." }));
|
|
643
|
+
const review = toolResultJson(await tools.review_session.handler({ sessionId: "session-local-only", format: "json" }));
|
|
644
|
+
assert.equal(capture.ok, true);
|
|
645
|
+
assert.match(decision.id, /^d_/);
|
|
646
|
+
assert.equal(review.ok, true);
|
|
647
|
+
assert.equal(calls.length, 0);
|
|
648
|
+
const store = LocalStore.open();
|
|
649
|
+
try {
|
|
650
|
+
const stats = store.stats();
|
|
651
|
+
assert.equal(stats.signals, 1);
|
|
652
|
+
assert.equal(stats.decisions, 1);
|
|
653
|
+
}
|
|
654
|
+
finally {
|
|
655
|
+
store.close();
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
test("installed free mode with credential file captures locally even if hosted app would return local-only 403", async () => {
|
|
660
|
+
await withInstalledFreeEnv(async () => {
|
|
661
|
+
const calls = [];
|
|
662
|
+
const server = createAskTheWMcpServer({
|
|
663
|
+
apiBaseUrl: "https://askthew.example.com",
|
|
664
|
+
fetchImpl: async (url) => {
|
|
665
|
+
calls.push({ url: String(url) });
|
|
666
|
+
return new Response(JSON.stringify({
|
|
667
|
+
ok: false,
|
|
668
|
+
code: "local_only_free_feature",
|
|
669
|
+
message: "This free MCP token is local-only and cannot read or write the shared Ask The W workspace.",
|
|
670
|
+
}), { status: 403 });
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
674
|
+
const tools = server._registeredTools;
|
|
675
|
+
const capture = toolResultJson(await tools.capture_session_signal.handler({
|
|
676
|
+
sessionId: "session-installed-free",
|
|
677
|
+
sequence: 1,
|
|
678
|
+
kind: "setup_complete",
|
|
679
|
+
summary: "Installed free mode should write this locally.",
|
|
680
|
+
evidence: [],
|
|
681
|
+
filesTouched: [],
|
|
682
|
+
commandsRun: [],
|
|
683
|
+
metadata: {},
|
|
684
|
+
}));
|
|
685
|
+
assert.equal(capture.ok, true);
|
|
686
|
+
assert.equal(capture.sessionId, "session-installed-free");
|
|
687
|
+
assert.equal("code" in capture, false);
|
|
688
|
+
assert.equal(JSON.stringify(capture).includes("local_only_free_feature"), false);
|
|
689
|
+
assert.equal(calls.length, 0);
|
|
690
|
+
const store = LocalStore.open();
|
|
691
|
+
try {
|
|
692
|
+
const signals = store.listSignals({ sessionId: "session-installed-free", limit: 10 });
|
|
693
|
+
assert.equal(signals.length, 1);
|
|
694
|
+
assert.equal(signals[0]?.summary, "Installed free mode should write this locally.");
|
|
695
|
+
}
|
|
696
|
+
finally {
|
|
697
|
+
store.close();
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
test("free decisions, recap, coach, and promote return human-readable compact outputs", async () => {
|
|
702
|
+
await withFreeEnv(async () => {
|
|
703
|
+
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
704
|
+
const tools = server._registeredTools;
|
|
705
|
+
await tools.capture_session_signal.handler({
|
|
706
|
+
sessionId: "session-free",
|
|
707
|
+
sequence: 1,
|
|
708
|
+
kind: "implementation_update",
|
|
709
|
+
summary: "Implemented the trial onboarding CTA.",
|
|
710
|
+
evidence: [],
|
|
711
|
+
filesTouched: ["apps/app/page.tsx"],
|
|
712
|
+
commandsRun: [],
|
|
713
|
+
metadata: {},
|
|
714
|
+
});
|
|
715
|
+
await tools.capture_session_signal.handler({
|
|
716
|
+
sessionId: "session-free",
|
|
717
|
+
sequence: 2,
|
|
718
|
+
kind: "verification_result",
|
|
719
|
+
summary: "npm test passed.",
|
|
720
|
+
evidence: [],
|
|
721
|
+
filesTouched: ["apps/app/page.tsx"],
|
|
722
|
+
commandsRun: ["npm test"],
|
|
723
|
+
metadata: {},
|
|
724
|
+
});
|
|
725
|
+
const created = toolResultJson(await tools.create_decision.handler({ content: "Keep the direct onboarding CTA." }));
|
|
726
|
+
assert.equal(created.ok, true);
|
|
727
|
+
assert.match(created.id, /^d_/);
|
|
728
|
+
assert.equal(created.sequence, 1);
|
|
729
|
+
const promoted = toolResultJson(await tools.promote_signal_to_decision.handler({ signalId: 1, status: "committed", why: "The captured implementation signal explains the change." }));
|
|
730
|
+
assert.equal(promoted.ok, true);
|
|
731
|
+
assert.equal(promoted.linkedSignalId, 1);
|
|
732
|
+
assert.equal(promoted.decision.rawContent, "Implemented the trial onboarding CTA.");
|
|
733
|
+
assert.deepEqual(promoted.decision.sourceSignalIds, [1]);
|
|
734
|
+
const digest = toolResultJson(await tools.recap.handler({ format: "digest" }));
|
|
735
|
+
const standup = toolResultJson(await tools.recap.handler({ format: "standup" }));
|
|
736
|
+
const share = toolResultJson(await tools.recap.handler({ format: "share" }));
|
|
737
|
+
assert.notEqual(digest.rendered, standup.rendered);
|
|
738
|
+
assert.notEqual(standup.rendered, share.rendered);
|
|
739
|
+
const coach = toolResultJson(await tools.coach.handler({ scope: "session" }));
|
|
740
|
+
assert.equal(coach.ok, true);
|
|
741
|
+
assert.equal(typeof coach.qualityScore, "number");
|
|
742
|
+
assert.match(coach.biggestGap, /\w/);
|
|
743
|
+
for (const scope of ["week", "patterns"]) {
|
|
744
|
+
const response = toolResultJson(await tools.coach.handler({ scope }));
|
|
745
|
+
assert.equal(response.ok, false);
|
|
746
|
+
assert.equal(response.code, "free_tier_paid_feature");
|
|
747
|
+
assert.equal(response.tool, "coach");
|
|
748
|
+
assert.match(response.upgradeUrl, /askthew\.com\/mcp/);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
test("free local tools surface decision candidates, compact views, search, traversal, conflicts, and max_chars", async () => {
|
|
753
|
+
await withFreeEnv(async () => {
|
|
754
|
+
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
755
|
+
const tools = server._registeredTools;
|
|
756
|
+
const captured = toolResultJson(await tools.capture_session_signal.handler({
|
|
757
|
+
sessionId: "session-candidates",
|
|
758
|
+
sequence: 1,
|
|
759
|
+
kind: "direction_change",
|
|
760
|
+
summary: "Let's go with token-budgeted local search because recap output is too large.",
|
|
761
|
+
evidence: [{
|
|
762
|
+
role: "assistant",
|
|
763
|
+
kind: "diff",
|
|
764
|
+
excerpt: "Changed search behavior.",
|
|
765
|
+
diff: "- old recap only\n+ new local search",
|
|
766
|
+
}],
|
|
767
|
+
filesTouched: ["packages/mcp-plugin/src/index.ts"],
|
|
768
|
+
commandsRun: [],
|
|
769
|
+
metadata: {},
|
|
770
|
+
}));
|
|
771
|
+
const candidates = toolResultJson(await tools.list_decision_candidates.handler({ sessionId: "session-candidates" }));
|
|
772
|
+
assert.equal(candidates.ok, true);
|
|
773
|
+
assert.equal(candidates.decisionCandidates.length, 1);
|
|
774
|
+
assert.equal(candidates.decisionCandidates[0].signalId, captured.id);
|
|
775
|
+
const promoted = toolResultJson(await tools.promote_signal_to_decision.handler({
|
|
776
|
+
signalId: captured.id,
|
|
777
|
+
status: "committed",
|
|
778
|
+
why: "The direction change has the rationale.",
|
|
779
|
+
}));
|
|
780
|
+
assert.equal(promoted.ok, true);
|
|
781
|
+
assert.equal(promoted.decision.contributingSignals[0].id, captured.id);
|
|
782
|
+
assert.equal(Boolean(promoted.decision.committedAt), true);
|
|
783
|
+
const signal = toolResultJson(await tools.get_signal.handler({ id: String(captured.id) }));
|
|
784
|
+
assert.equal(signal.signal.decision.id, promoted.id);
|
|
785
|
+
const decision = toolResultJson(await tools.get_decision.handler({ id: promoted.id }));
|
|
786
|
+
assert.equal(decision.decision.contributingSignals[0].id, captured.id);
|
|
787
|
+
const compact = toolResultJson(await tools.list_signals.handler({ sessionId: "session-candidates", compact: true }));
|
|
788
|
+
assert.deepEqual(Object.keys(compact.signals[0]).sort(), ["decisionId", "files", "id", "kind", "summary"]);
|
|
789
|
+
const found = toolResultJson(await tools.find_signal_by_summary.handler({ query: "token-budgeted" }));
|
|
790
|
+
assert.equal(found.ok, true);
|
|
791
|
+
assert.equal(found.signals[0].id, captured.id);
|
|
792
|
+
const search = toolResultJson(await tools.search_trail.handler({ query: "local search", compact: true }));
|
|
793
|
+
assert.equal(search.ok, true);
|
|
794
|
+
assert.equal(search.matches.length >= 1, true);
|
|
795
|
+
const conflict = toolResultJson(await tools.create_decision.handler({
|
|
796
|
+
content: "Drop token-budgeted local search.",
|
|
797
|
+
}));
|
|
798
|
+
assert.equal(conflict.ok, true);
|
|
799
|
+
assert.equal(conflict.warnings[0].code, "possible_conflict");
|
|
800
|
+
assert.equal(conflict.warnings[0].conflictingDecisionId, promoted.id);
|
|
801
|
+
const budgeted = toolResultJson(await tools.review_session.handler({
|
|
802
|
+
sessionId: "session-candidates",
|
|
803
|
+
format: "json",
|
|
804
|
+
max_chars: 500,
|
|
805
|
+
}));
|
|
806
|
+
assert.equal(budgeted.ok, true);
|
|
807
|
+
assert.equal(budgeted.truncated, true);
|
|
808
|
+
assert.equal(JSON.stringify(budgeted, null, 2).length <= 700, true);
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
test("free-tier smoke covers every free tool without overflow or silent failures", async () => {
|
|
812
|
+
await withFreeEnv(async () => {
|
|
813
|
+
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
814
|
+
const tools = server._registeredTools;
|
|
815
|
+
const results = {};
|
|
816
|
+
const capture = toolResultJson(await tools.capture_session_signal.handler({
|
|
817
|
+
sessionId: "session-smoke",
|
|
818
|
+
sequence: 1,
|
|
819
|
+
kind: "implementation_update",
|
|
820
|
+
summary: "Implemented every free tool smoke path.",
|
|
821
|
+
evidence: [],
|
|
822
|
+
filesTouched: ["packages/mcp-plugin/src/index.ts"],
|
|
823
|
+
commandsRun: ["npm test --workspace @askthew/mcp-plugin"],
|
|
824
|
+
metadata: {},
|
|
825
|
+
}));
|
|
826
|
+
results.capture_session_signal = capture;
|
|
827
|
+
assert.equal(capture.ok, true);
|
|
828
|
+
results.list_signals = toolResultJson(await tools.list_signals.handler({ limit: 10 }));
|
|
829
|
+
assert.equal(results.list_signals.ok, true);
|
|
830
|
+
assert.equal(results.list_signals.signals.length, 1);
|
|
831
|
+
results.get_signal = toolResultJson(await tools.get_signal.handler({ id: String(capture.id) }));
|
|
832
|
+
assert.equal(results.get_signal.ok, true);
|
|
833
|
+
assert.equal(results.get_signal.signal.summary, "Implemented every free tool smoke path.");
|
|
834
|
+
const created = toolResultJson(await tools.create_decision.handler({ content: "Keep free-tool smoke coverage explicit.", echo: "summary" }));
|
|
835
|
+
results.create_decision = created;
|
|
836
|
+
assert.equal(created.ok, true);
|
|
837
|
+
assert.match(created.id, /^d_/);
|
|
838
|
+
results.get_decision = toolResultJson(await tools.get_decision.handler({ id: created.id }));
|
|
839
|
+
assert.equal(results.get_decision.ok, true);
|
|
840
|
+
results.update_decision = toolResultJson(await tools.update_decision.handler({
|
|
841
|
+
id: created.id,
|
|
842
|
+
headline: "Keep free-tool smoke coverage explicit",
|
|
843
|
+
why: "Acceptance requires every free tool to run.",
|
|
844
|
+
status: "committed",
|
|
845
|
+
echo: "summary",
|
|
846
|
+
}));
|
|
847
|
+
assert.equal(results.update_decision.ok, true);
|
|
848
|
+
results.list_decisions = toolResultJson(await tools.list_decisions.handler({ limit: 10 }));
|
|
849
|
+
assert.equal(results.list_decisions.ok, true);
|
|
850
|
+
assert.equal(results.list_decisions.decisions.length >= 1, true);
|
|
851
|
+
results.review_decisions = toolResultJson(await tools.review_decisions.handler({ format: "markdown", limit: 10 }));
|
|
852
|
+
assert.equal(results.review_decisions.ok, true);
|
|
853
|
+
assert.match(results.review_decisions.rendered, /# Decisions/);
|
|
854
|
+
results.review_session = toolResultJson(await tools.review_session.handler({ sessionId: "session-smoke", format: "markdown" }));
|
|
855
|
+
assert.equal(results.review_session.ok, true);
|
|
856
|
+
assert.match(results.review_session.rendered, /Session Review/);
|
|
857
|
+
results.view_timeline = toolResultJson(await tools.view_timeline.handler({ scope: "session" }));
|
|
858
|
+
assert.equal(results.view_timeline.ok, true);
|
|
859
|
+
assert.equal(results.view_timeline.points.some((point) => point.x === "Other"), false);
|
|
860
|
+
results.recap = toolResultJson(await tools.recap.handler({ sessionId: "session-smoke", format: "digest" }));
|
|
861
|
+
assert.equal(results.recap.ok, true);
|
|
862
|
+
assert.match(results.recap.rendered, /Session Digest/);
|
|
863
|
+
results.coach = toolResultJson(await tools.coach.handler({ sessionId: "session-smoke", scope: "session" }));
|
|
864
|
+
assert.equal(results.coach.ok, true);
|
|
865
|
+
assert.match(results.coach.rendered, /Decision quality score/);
|
|
866
|
+
results.promote_signal_to_decision = toolResultJson(await tools.promote_signal_to_decision.handler({
|
|
867
|
+
signalId: capture.id,
|
|
868
|
+
status: "committed",
|
|
869
|
+
why: "The signal records the implementation.",
|
|
870
|
+
}));
|
|
871
|
+
assert.equal(results.promote_signal_to_decision.ok, true);
|
|
872
|
+
assert.equal(results.promote_signal_to_decision.linkedSignalId, capture.id);
|
|
873
|
+
results.delete_decision = toolResultJson(await tools.delete_decision.handler({
|
|
874
|
+
id: created.id,
|
|
875
|
+
confirmText: created.id,
|
|
876
|
+
}));
|
|
877
|
+
assert.equal(results.delete_decision.ok, true);
|
|
878
|
+
for (const [toolName, result] of Object.entries(results)) {
|
|
879
|
+
assert.equal(result.ok, true, toolName);
|
|
880
|
+
assert.equal(JSON.stringify(result).length < 15000, true, `${toolName} should stay compact`);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
test("free review_session markdown is capped and json is cursor-paginated with a 3-session limit", async () => {
|
|
885
|
+
await withFreeEnv(async () => {
|
|
886
|
+
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
887
|
+
const tools = server._registeredTools;
|
|
888
|
+
for (let sessionIndex = 1; sessionIndex <= 4; sessionIndex += 1) {
|
|
889
|
+
for (let sequence = 1; sequence <= 55; sequence += 1) {
|
|
890
|
+
await tools.capture_session_signal.handler({
|
|
891
|
+
sessionId: `session-${sessionIndex}`,
|
|
892
|
+
sequence,
|
|
893
|
+
kind: sequence % 5 === 0 ? "verification_result" : "session_checkpoint",
|
|
894
|
+
summary: `Signal ${sequence} for session ${sessionIndex}`,
|
|
895
|
+
evidence: [],
|
|
896
|
+
filesTouched: [],
|
|
897
|
+
commandsRun: [],
|
|
898
|
+
metadata: {},
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const markdown = toolResultJson(await tools.review_session.handler({ sessionId: "session-4", format: "markdown" }));
|
|
903
|
+
assert.equal(markdown.ok, true);
|
|
904
|
+
assert.equal(markdown.rendered.split("\n").length <= 300, true);
|
|
905
|
+
assert.match(markdown.rendered, /Signals By Kind/);
|
|
906
|
+
assert.equal("signals" in markdown, false);
|
|
907
|
+
const json = toolResultJson(await tools.review_session.handler({ sessionId: "session-4", format: "json" }));
|
|
908
|
+
assert.equal(json.ok, true);
|
|
909
|
+
assert.equal(json.signals.length, 50);
|
|
910
|
+
assert.equal(typeof json.nextCursor, "string");
|
|
911
|
+
const capped = toolResultJson(await tools.review_session.handler({ sessionId: "session-1", format: "markdown" }));
|
|
912
|
+
assert.equal(capped.ok, false);
|
|
913
|
+
assert.equal(capped.code, "free_tier_limit");
|
|
914
|
+
assert.equal(capped.limit, 3);
|
|
915
|
+
assert.match(capped.upgradeUrl, /askthew\.com\/mcp/);
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
test("paid tools return canonical paywall envelope before transport in free mode", async () => {
|
|
919
|
+
await withFreeEnv(async () => {
|
|
920
|
+
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
921
|
+
const tools = server._registeredTools;
|
|
922
|
+
for (const toolName of [
|
|
923
|
+
"list_outcomes",
|
|
924
|
+
"get_outcome",
|
|
925
|
+
"list_outcome_signals",
|
|
926
|
+
"create_outcome",
|
|
927
|
+
"update_outcome",
|
|
928
|
+
"delete_outcome",
|
|
929
|
+
"get_north_star",
|
|
930
|
+
"update_north_star",
|
|
931
|
+
"export_decisions",
|
|
932
|
+
]) {
|
|
933
|
+
const payload = toolName === "get_outcome" || toolName === "list_outcome_signals"
|
|
934
|
+
? { id: "o1" }
|
|
935
|
+
: toolName === "create_outcome"
|
|
936
|
+
? { name: "Reduce churn" }
|
|
937
|
+
: toolName === "update_outcome"
|
|
938
|
+
? { id: "o1", summary: "New" }
|
|
939
|
+
: toolName === "delete_outcome"
|
|
940
|
+
? { id: "o1", confirmText: "Reduce churn" }
|
|
941
|
+
: toolName === "update_north_star"
|
|
942
|
+
? { metric: "Active users", current: "1", target: "10", reason: "test" }
|
|
943
|
+
: {};
|
|
944
|
+
const response = toolResultJson(await tools[toolName].handler(payload));
|
|
945
|
+
assert.equal(response.ok, false, toolName);
|
|
946
|
+
assert.equal(response.code, "free_tier_paid_feature", toolName);
|
|
947
|
+
assert.equal(response.tool, toolName, toolName);
|
|
948
|
+
assert.match(response.upgradeUrl, /askthew\.com\/mcp/, toolName);
|
|
949
|
+
assert.equal(response.supportEmail, "support@askthew.com", toolName);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
});
|