taskover-mcp 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Build the publish artifact for taskover-mcp npm package.
5
+ # This script assembles a clean directory containing ONLY cloud-mode files.
6
+ # Local-mode files (data-store.js, db.js, etc.) are NEVER copied.
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ MCP_DIR="$(dirname "$SCRIPT_DIR")"
10
+ PUBLISH_DIR="$MCP_DIR/publish"
11
+
12
+ echo "[build] Cleaning publish directory..."
13
+ mkdir -p "$PUBLISH_DIR"
14
+ rm -f "$PUBLISH_DIR"/* 2>/dev/null || true
15
+
16
+ echo "[build] Copying cloud-mode files..."
17
+ cp "$MCP_DIR/index.js" "$PUBLISH_DIR/index.js"
18
+ cp "$MCP_DIR/cloud-adapter.js" "$PUBLISH_DIR/cloud-adapter.js"
19
+ cp "$MCP_DIR/tool-map.js" "$PUBLISH_DIR/tool-map.js"
20
+
21
+ echo "[build] Writing package.json..."
22
+ cat > "$PUBLISH_DIR/package.json" << 'PKGJSON'
23
+ {
24
+ "name": "taskover-mcp",
25
+ "version": "1.0.0",
26
+ "description": "MCP server for TaskOver.gg - connect your AI assistant to your game dev projects",
27
+ "main": "index.js",
28
+ "bin": "index.js",
29
+ "keywords": ["mcp", "taskover", "gamedev", "project-management", "claude"],
30
+ "author": "TaskOver, LLC <hello@taskover.gg>",
31
+ "license": "MIT",
32
+ "homepage": "https://taskover.gg",
33
+ "repository": { "type": "git", "url": "https://github.com/taskover/mcp" },
34
+ "engines": { "node": ">=20.0.0" },
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.0.0"
37
+ }
38
+ }
39
+ PKGJSON
40
+
41
+ echo "[build] Writing README..."
42
+ cat > "$PUBLISH_DIR/README.md" << 'README'
43
+ # taskover-mcp
44
+
45
+ MCP server for [TaskOver.gg](https://taskover.gg) - connect Claude Desktop, Claude Code, Cursor, or Windsurf to your game dev project data.
46
+
47
+ ## Setup
48
+
49
+ 1. Get your API key from taskover.gg -> Settings -> Security
50
+ 2. Add to your AI host config:
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "taskover": {
56
+ "command": "npx",
57
+ "args": ["-y", "taskover-mcp@latest"],
58
+ "env": {
59
+ "TASKOVER_API_KEY": "tok_paste-your-key-here"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ 3. Restart your AI host
67
+
68
+ Your API key is stored in plaintext in the config file. For better security, use browser auth (the default) — it stores tokens in your OS keychain instead.
69
+ README
70
+
71
+ # SECURITY: Verify no local-mode files leaked into publish directory
72
+ echo "[build] Safety check: verifying no local-mode files present..."
73
+ for banned in data-store.js db.js "better-sqlite3" "package-lock.json"; do
74
+ if [ -e "$PUBLISH_DIR/$banned" ]; then
75
+ echo "[FATAL] Local-mode file '$banned' found in publish directory!"
76
+ exit 1
77
+ fi
78
+ done
79
+
80
+ # Verify cloud-adapter.js doesn't require data-store or db
81
+ if grep -q "require.*data-store\|require.*\/db" "$PUBLISH_DIR/cloud-adapter.js"; then
82
+ echo "[FATAL] cloud-adapter.js contains local-mode require!"
83
+ exit 1
84
+ fi
85
+
86
+ echo "[build] Checking index.js for local-mode requires in cloud path..."
87
+ # This is a rough check -- the actual cloud-mode code path must not require data-store
88
+ # The local-mode require is behind an if (LOCAL_MODE) guard, which is acceptable
89
+
90
+ echo "[build] Publish directory ready at: $PUBLISH_DIR"
91
+ echo "[build] Contents:"
92
+ ls -la "$PUBLISH_DIR"
93
+ echo ""
94
+ echo "[build] To publish: cd $PUBLISH_DIR && npm publish"
95
+ echo "[build] To dry-run: cd $PUBLISH_DIR && npm publish --dry-run"
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ // test-auth-failure.js — Auth failure tests for all 52 write tools + 5 sample reads
3
+ // Requires network access to api.taskover.gg (uses an invalid key)
4
+ // Usage: node mcp-server/scripts/test-auth-failure.js
5
+ //
6
+ // Tests: Every write method with a bad key → AUTH_FAILED, no key in error
7
+
8
+ "use strict";
9
+
10
+ const adapter = require("../cloud-adapter.js");
11
+ const { MCP_ALLOWED_WRITES, MCP_ALLOWED_READS } = require("../cloud-adapter.js");
12
+
13
+ const BAD_KEY = "tok_definitely_invalid_for_auth_test";
14
+ adapter.init(BAD_KEY);
15
+
16
+ const results = [];
17
+
18
+ // Sample 5 reads for auth failure verification
19
+ const sampleReads = ["listProjects", "getTasks", "getBugs", "getSystems", "dashboard"];
20
+
21
+ async function testMethod(method, args, type) {
22
+ try {
23
+ await adapter.callRpc(method, args);
24
+ results.push({ method, type, status: "FAIL", reason: "did not throw" });
25
+ } catch (err) {
26
+ const isAuthFailed = err.message === "AUTH_FAILED";
27
+ const keyLeaked = err.message.includes(BAD_KEY) || (err.stack && err.stack.includes(BAD_KEY));
28
+ if (isAuthFailed && !keyLeaked) {
29
+ results.push({ method, type, status: "PASS", reason: "" });
30
+ } else if (!isAuthFailed) {
31
+ results.push({ method, type, status: "FAIL", reason: `expected AUTH_FAILED, got: ${err.message.slice(0, 80)}` });
32
+ } else {
33
+ results.push({ method, type, status: "FAIL", reason: "key leaked in error" });
34
+ }
35
+ }
36
+ }
37
+
38
+ async function run() {
39
+ console.log("Testing auth failure with invalid key...\n");
40
+
41
+ // Test all 52 writes
42
+ for (const method of MCP_ALLOWED_WRITES) {
43
+ await testMethod(method, [{ projectId: "test", title: "should-fail" }], "write");
44
+ }
45
+
46
+ // Sample 5 reads
47
+ for (const method of sampleReads) {
48
+ const args = method === "listProjects" ? [] : ["test"];
49
+ await testMethod(method, args, "read-sample");
50
+ }
51
+
52
+ // Output
53
+ const passed = results.filter(r => r.status === "PASS").length;
54
+ const failed = results.filter(r => r.status === "FAIL").length;
55
+
56
+ console.log("\n| # | Method | Type | Status | Notes |");
57
+ console.log("|---|--------|------|--------|-------|");
58
+ results.forEach((r, i) => {
59
+ console.log(`| ${i + 1} | ${r.method} | ${r.type} | ${r.status} | ${r.reason} |`);
60
+ });
61
+
62
+ console.log(`\nPASS: ${passed} FAIL: ${failed} TOTAL: ${results.length}`);
63
+ console.log(`Writes: ${[...MCP_ALLOWED_WRITES].length} Read samples: ${sampleReads.length}`);
64
+
65
+ if (failed > 0) process.exit(1);
66
+ }
67
+
68
+ run().catch(err => { console.error(err); process.exit(1); });
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+ // test-success.js — Live success tests for all 85 cloud MCP tools
3
+ // Requires: TASKOVER_API_KEY (valid key), TEST_PROJECT_ID (project with data)
4
+ // Usage: TASKOVER_API_KEY=tok_xxx TEST_PROJECT_ID=village node mcp-server/scripts/test-success.js
5
+ //
6
+ // Tests:
7
+ // - Read tools: callRpc returns data without error
8
+ // - Write tools: callRpc succeeds, returns non-null
9
+ // - Roundtrip: add methods create records that appear in corresponding get
10
+
11
+ "use strict";
12
+
13
+ const adapter = require("../cloud-adapter.js");
14
+ const { TOOL_MAP } = require("../tool-map.js");
15
+
16
+ const API_KEY = process.env.TASKOVER_API_KEY;
17
+ const PID = process.env.TEST_PROJECT_ID;
18
+
19
+ if (!API_KEY) { console.error("Set TASKOVER_API_KEY"); process.exit(1); }
20
+ if (!PID) { console.error("Set TEST_PROJECT_ID"); process.exit(1); }
21
+
22
+ adapter.init(API_KEY);
23
+
24
+ // Created record IDs from add operations — used by update/record tests
25
+ const created = {};
26
+
27
+ // ── Test data per tool ──
28
+ // Each entry: { args: [...], expect: "array"|"object"|"string"|"any", skip?: true, reason?: string }
29
+ // "any" means we just check for no error.
30
+
31
+ function testData(toolName, mapping) {
32
+ const rpc = mapping.rpc;
33
+ const ts = Date.now().toString(36);
34
+
35
+ // ── Reads ──
36
+ if (rpc === "listProjects") return { args: [], expect: "array" };
37
+ if (rpc === "dashboard") return { args: [PID], expect: "object" };
38
+ if (rpc === "contextExport") return { args: [PID], expect: "any" };
39
+ if (rpc === "search") return { args: [PID, "test"], expect: "any" };
40
+ if (rpc === "getTasks") return { args: [PID], expect: "array" };
41
+ if (rpc === "getBugs") return { args: [PID], expect: "array" };
42
+ if (rpc === "getSystems") return { args: [PID], expect: "array" };
43
+ if (rpc === "getMilestones") return { args: [PID], expect: "array" };
44
+ if (rpc === "getSessions") return { args: [PID], expect: "array" };
45
+ if (rpc === "getChangelog") return { args: [PID], expect: "array" };
46
+ if (rpc === "getDecisions") return { args: [PID], expect: "array" };
47
+ if (rpc === "getBlueprints") return { args: [PID], expect: "array" };
48
+ if (rpc === "getNotes") return { args: [PID], expect: "array" };
49
+ if (rpc === "getBuildErrors") return { args: [PID], expect: "array" };
50
+ if (rpc === "getAssets") return { args: [PID], expect: "array" };
51
+ if (rpc === "getOptimizeItems") return { args: [PID], expect: "array" };
52
+ if (rpc === "getPerfBudget") return { args: [PID], expect: "array" };
53
+ if (rpc === "getLevels") return { args: [PID], expect: "array" };
54
+ if (rpc === "getMarketingItems") return { args: [PID], expect: "array" };
55
+ if (rpc === "getDialogues") return { args: [PID], expect: "array" };
56
+ if (rpc === "getSounds") return { args: [PID], expect: "array" };
57
+ if (rpc === "getControls") return { args: [PID], expect: "array" };
58
+ if (rpc === "getRefs") return { args: [PID], expect: "array" };
59
+ if (rpc === "getPlugins") return { args: [PID], expect: "array" };
60
+ if (rpc === "getShipChecked") return { args: [PID], expect: "any" };
61
+ if (rpc === "getIterations") return { args: [PID], expect: "array" };
62
+ if (rpc === "getPlaytests") return { args: [PID], expect: "array" };
63
+ if (rpc === "getOpenQuestions") return { args: [PID], expect: "array" };
64
+ if (rpc === "getWikiPages") return { args: [PID], expect: "array" };
65
+ if (rpc === "getStories") return { args: [PID], expect: "array" };
66
+ if (rpc === "getScenes") return { args: [PID], expect: "array" };
67
+ if (rpc === "getStoryBible") return { args: [PID], expect: "any" };
68
+
69
+ // getSceneContent needs a scene_id — defer to after addScene
70
+ if (rpc === "getSceneContent") return { args: [], expect: "any", deferred: "scene" };
71
+
72
+ // ── Writes: add methods ──
73
+ if (rpc === "addTask") return { args: [{ projectId: PID, title: `cloud-test-${ts}`, status: "todo", priority: "medium", createdBy: "Assistant" }], expect: "any", track: "task" };
74
+ if (rpc === "addBug") return { args: [{ projectId: PID, title: `cloud-bug-${ts}` }], expect: "any", track: "bug" };
75
+ if (rpc === "addSystem") return { args: [{ projectId: PID, name: `cloud-sys-${ts}` }], expect: "any", track: "system" };
76
+ if (rpc === "logSession") return { args: [{ projectId: PID, title: `cloud-session-${ts}` }], expect: "any" };
77
+ if (rpc === "addChangelog") return { args: [{ projectId: PID, title: `cloud-cl-${ts}`, changes: "test" }], expect: "any" };
78
+ if (rpc === "logDecision") return { args: [{ projectId: PID, decision: `cloud-dec-${ts}` }], expect: "any" };
79
+ if (rpc === "addBlueprint") return { args: [{ projectId: PID, name: `cloud-bp-${ts}` }], expect: "any", track: "blueprint" };
80
+ if (rpc === "addNote") return { args: [{ projectId: PID, content: `cloud-note-${ts}` }], expect: "any" };
81
+ if (rpc === "addBuildError") return { args: [{ projectId: PID, error: `cloud-err-${ts}` }], expect: "any", track: "buildError" };
82
+ if (rpc === "addLevel") return { args: [{ projectId: PID, name: `cloud-lvl-${ts}` }], expect: "any", track: "level" };
83
+ if (rpc === "addPlugin") return { args: [{ projectId: PID, name: `cloud-plug-${ts}` }], expect: "any", track: "plugin" };
84
+ if (rpc === "addOptimizeItem") return { args: [{ projectId: PID, title: `cloud-opt-${ts}` }], expect: "any", track: "optimizeItem" };
85
+ if (rpc === "addPerfBudget") return { args: [{ projectId: PID, scene: `cloud-perf-${ts}` }], expect: "any" };
86
+ if (rpc === "addPlaytest") return { args: [{ projectId: PID }], expect: "any" };
87
+ if (rpc === "addMilestone") return { args: [{ projectId: PID, title: `cloud-ms-${ts}` }], expect: "any", track: "milestone" };
88
+ if (rpc === "addIteration") return { args: [{ projectId: PID, feature: `cloud-iter-${ts}` }], expect: "any" };
89
+ if (rpc === "addDialogue") return { args: [{ projectId: PID, npc: `cloud-npc-${ts}` }], expect: "any", track: "dialogue" };
90
+ if (rpc === "addSound") return { args: [{ projectId: PID, name: `cloud-snd-${ts}` }], expect: "any", track: "sound" };
91
+ if (rpc === "addControl") return { args: [{ projectId: PID, key: "W", action: `cloud-ctrl-${ts}` }], expect: "any", track: "control" };
92
+ if (rpc === "addAsset") return { args: [{ projectId: PID, name: `cloud-asset-${ts}` }], expect: "any", track: "asset" };
93
+ if (rpc === "addRef") return { args: [{ projectId: PID, title: `cloud-ref-${ts}` }], expect: "any" };
94
+ if (rpc === "addMarketingItem") return { args: [{ projectId: PID, item: `cloud-mkt-${ts}` }], expect: "any", track: "marketingItem" };
95
+ if (rpc === "addWikiPage") return { args: [{ projectId: PID, title: `cloud-wiki-${ts}` }], expect: "any", track: "wikiPage" };
96
+ if (rpc === "addOpenQuestion") return { args: [{ projectId: PID, text: `cloud-q-${ts}` }], expect: "any", track: "question" };
97
+ if (rpc === "addStory") return { args: [{ projectId: PID, sectionId: `s-${ts}`, section: `cloud-story-${ts}` }], expect: "any", track: "story" };
98
+ if (rpc === "addScene") return { args: [{ projectId: PID, title: `cloud-scene-${ts}` }], expect: "any", track: "scene" };
99
+ if (rpc === "addStoryCharacter") return { args: [PID, { name: `cloud-char-${ts}` }], expect: "any" };
100
+ if (rpc === "logBackup") return { args: [{ projectId: PID, title: `cloud-bak-${ts}` }], expect: "any" };
101
+
102
+ // ── Writes: update/record methods (need created IDs) ──
103
+ if (rpc === "updateTask") return { args: [], expect: "any", deferred: "task" };
104
+ if (rpc === "moveTask") return { args: [], expect: "any", deferred: "task" };
105
+ if (rpc === "addTaskComment") return { args: [], expect: "any", deferred: "task" };
106
+ if (rpc === "fixBug") return { args: [], expect: "any", deferred: "bug" };
107
+ if (rpc === "updateSystem") return { args: [], expect: "any", deferred: "system" };
108
+ if (rpc === "updateBlueprint") return { args: [], expect: "any", deferred: "blueprint" };
109
+ if (rpc === "fixBuildError") return { args: [], expect: "any", deferred: "buildError" };
110
+ if (rpc === "updateLevel") return { args: [], expect: "any", deferred: "level" };
111
+ if (rpc === "addActor") return { args: [], expect: "any", deferred: "level" };
112
+ if (rpc === "removeActor") return { args: [], expect: "any", deferred: "level" };
113
+ if (rpc === "updatePlugin") return { args: [], expect: "any", deferred: "plugin" };
114
+ if (rpc === "updateOptimizeItem") return { args: [], expect: "any", deferred: "optimizeItem" };
115
+ if (rpc === "updateMilestone") return { args: [], expect: "any", deferred: "milestone" };
116
+ if (rpc === "updateDialogue") return { args: [], expect: "any", deferred: "dialogue" };
117
+ if (rpc === "updateSound") return { args: [], expect: "any", deferred: "sound" };
118
+ if (rpc === "updateControl") return { args: [], expect: "any", deferred: "control" };
119
+ if (rpc === "updateAsset") return { args: [], expect: "any", deferred: "asset" };
120
+ if (rpc === "updateMarketingItem") return { args: [], expect: "any", deferred: "marketingItem" };
121
+ if (rpc === "updateWikiPage") return { args: [], expect: "any", deferred: "wikiPage" };
122
+ if (rpc === "updateStoryBible") return { args: [PID, { title: `bible-${ts}` }], expect: "any" };
123
+ if (rpc === "updateStory") return { args: [], expect: "any", deferred: "story" };
124
+ if (rpc === "updateScene") return { args: [], expect: "any", deferred: "scene" };
125
+ if (rpc === "toggleShipCheck") return { args: [PID, "perf-check"], expect: "any" };
126
+ if (rpc === "toggleQuestion") return { args: [], expect: "any", deferred: "question" };
127
+
128
+ return { args: [], expect: "any", skip: true, reason: `no test data for ${rpc}` };
129
+ }
130
+
131
+ // Build deferred args once we have created IDs
132
+ function deferredArgs(rpc, id) {
133
+ const ts = Date.now().toString(36);
134
+ if (rpc === "updateTask") return [id, { title: `updated-${ts}` }];
135
+ if (rpc === "moveTask") return [id, "doing"];
136
+ if (rpc === "addTaskComment") return [id, `comment-${ts}`];
137
+ if (rpc === "fixBug") return [id, `fixed-${ts}`];
138
+ if (rpc === "updateSystem") return [id, { name: `updated-${ts}` }];
139
+ if (rpc === "updateBlueprint") return [id, { name: `updated-${ts}` }];
140
+ if (rpc === "fixBuildError") return [id, `fix-${ts}`];
141
+ if (rpc === "updateLevel") return [id, { name: `updated-${ts}` }];
142
+ if (rpc === "addActor") return [id, { name: `actor-${ts}` }];
143
+ if (rpc === "removeActor") return [id, 0];
144
+ if (rpc === "updatePlugin") return [id, { name: `updated-${ts}` }];
145
+ if (rpc === "updateOptimizeItem") return [id, { title: `updated-${ts}` }];
146
+ if (rpc === "updateMilestone") return [id, { title: `updated-${ts}` }];
147
+ if (rpc === "updateDialogue") return [id, { npc: `updated-${ts}` }];
148
+ if (rpc === "updateSound") return [id, { name: `updated-${ts}` }];
149
+ if (rpc === "updateControl") return [id, { key: "E" }];
150
+ if (rpc === "updateAsset") return [id, { name: `updated-${ts}` }];
151
+ if (rpc === "updateMarketingItem") return [id, { item: `updated-${ts}` }];
152
+ if (rpc === "updateWikiPage") return [id, { title: `updated-${ts}` }];
153
+ if (rpc === "updateStory") return [id, { section: `updated-${ts}` }];
154
+ if (rpc === "updateScene") return [id, { title: `updated-${ts}` }];
155
+ if (rpc === "toggleQuestion") return [id];
156
+ if (rpc === "getSceneContent") return [id];
157
+ return null;
158
+ }
159
+
160
+ async function run() {
161
+ console.log(`Testing ${Object.keys(TOOL_MAP).length} tools against ${PID}...\n`);
162
+
163
+ const results = [];
164
+ const deferred = []; // { toolName, rpc, dependsOn }
165
+
166
+ // Phase 1: run reads + adds (collect IDs)
167
+ for (const [toolName, mapping] of Object.entries(TOOL_MAP)) {
168
+ const td = testData(toolName, mapping);
169
+ if (td.skip) {
170
+ results.push({ tool: toolName, rpc: mapping.rpc, status: "SKIP", reason: td.reason });
171
+ continue;
172
+ }
173
+ if (td.deferred) {
174
+ deferred.push({ toolName, rpc: mapping.rpc, dependsOn: td.deferred });
175
+ continue;
176
+ }
177
+
178
+ try {
179
+ const result = await adapter.callRpc(mapping.rpc, td.args);
180
+ let ok = true;
181
+ if (td.expect === "array" && !Array.isArray(result)) ok = false;
182
+ if (td.expect === "object" && (typeof result !== "object" || Array.isArray(result))) ok = false;
183
+
184
+ if (td.track && result && typeof result === "object") {
185
+ created[td.track] = result.id || result.insertedId || null;
186
+ }
187
+
188
+ results.push({ tool: toolName, rpc: mapping.rpc, status: ok ? "PASS" : "FAIL", reason: ok ? "" : `expected ${td.expect}, got ${typeof result}` });
189
+ } catch (err) {
190
+ results.push({ tool: toolName, rpc: mapping.rpc, status: "FAIL", reason: err.message.slice(0, 120) });
191
+ }
192
+ }
193
+
194
+ // Phase 2: run deferred (update/record methods using created IDs)
195
+ for (const { toolName, rpc, dependsOn } of deferred) {
196
+ const id = created[dependsOn];
197
+ if (!id) {
198
+ results.push({ tool: toolName, rpc, status: "SKIP", reason: `no ${dependsOn} ID from add phase` });
199
+ continue;
200
+ }
201
+
202
+ const args = deferredArgs(rpc, id);
203
+ if (!args) {
204
+ results.push({ tool: toolName, rpc, status: "SKIP", reason: `no deferred test data for ${rpc}` });
205
+ continue;
206
+ }
207
+
208
+ try {
209
+ await adapter.callRpc(rpc, args);
210
+ results.push({ tool: toolName, rpc, status: "PASS", reason: "" });
211
+ } catch (err) {
212
+ results.push({ tool: toolName, rpc, status: "FAIL", reason: err.message.slice(0, 120) });
213
+ }
214
+ }
215
+
216
+ // ── Output ──
217
+ const passed = results.filter(r => r.status === "PASS").length;
218
+ const failed = results.filter(r => r.status === "FAIL").length;
219
+ const skipped = results.filter(r => r.status === "SKIP").length;
220
+
221
+ console.log("\n| # | Tool | RPC | Status | Notes |");
222
+ console.log("|---|------|-----|--------|-------|");
223
+ results.forEach((r, i) => {
224
+ console.log(`| ${i + 1} | ${r.tool} | ${r.rpc} | ${r.status} | ${r.reason} |`);
225
+ });
226
+
227
+ console.log(`\nPASS: ${passed} FAIL: ${failed} SKIP: ${skipped} TOTAL: ${results.length}`);
228
+
229
+ if (failed > 0) process.exit(1);
230
+ }
231
+
232
+ run().catch(err => { console.error(err); process.exit(1); });
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ // test-validation.js — Schema validation tests for all 85 cloud MCP tools
3
+ // Runs locally, no API key or network needed.
4
+ // Usage: node mcp-server/scripts/test-validation.js
5
+ //
6
+ // Tests per tool:
7
+ // 1. Missing each required field → errors
8
+ // 2. Wrong type on first string field → errors
9
+ // 3. Unknown field injection → stripped from cleaned output
10
+
11
+ "use strict";
12
+
13
+ const { TOOL_MAP, validateSchema } = require("../tool-map.js");
14
+
15
+ let pass = 0;
16
+ let fail = 0;
17
+ const failures = [];
18
+
19
+ function assert(cond, toolName, msg) {
20
+ if (cond) { pass++; return; }
21
+ fail++;
22
+ failures.push(`${toolName}: ${msg}`);
23
+ }
24
+
25
+ // Build minimal valid args for a given schema
26
+ function buildValidArgs(schema) {
27
+ const args = {};
28
+ for (const [field, type] of Object.entries(schema.fields || {})) {
29
+ if (type === "string") args[field] = `test_${field}`;
30
+ else if (type === "number") args[field] = 1;
31
+ else if (type === "boolean") args[field] = true;
32
+ else if (type === "array") args[field] = [];
33
+ else if (type === "object") args[field] = {};
34
+ else args[field] = `test_${field}`;
35
+ }
36
+ return args;
37
+ }
38
+
39
+ // Find first string-typed field in schema
40
+ function firstStringField(schema) {
41
+ for (const [field, type] of Object.entries(schema.fields || {})) {
42
+ if (type === "string") return field;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ const toolNames = Object.keys(TOOL_MAP);
48
+ console.log(`Testing ${toolNames.length} tools...\n`);
49
+
50
+ for (const name of toolNames) {
51
+ const mapping = TOOL_MAP[name];
52
+ const { schema } = mapping;
53
+ const validArgs = buildValidArgs(schema);
54
+
55
+ // ── Test 1: Missing required fields ──
56
+ for (const field of (schema.required || [])) {
57
+ const args = { ...validArgs };
58
+ delete args[field];
59
+ const { errors } = validateSchema(args, schema);
60
+ assert(
61
+ errors.length > 0,
62
+ name,
63
+ `should reject missing required field: ${field}`
64
+ );
65
+ assert(
66
+ errors.some(e => e.includes(field)),
67
+ name,
68
+ `error message should name the missing field: ${field}`
69
+ );
70
+ }
71
+
72
+ // ── Test 2: Wrong type ──
73
+ const sf = firstStringField(schema);
74
+ if (sf) {
75
+ const args = { ...validArgs, [sf]: 99999 };
76
+ const { errors } = validateSchema(args, schema);
77
+ assert(
78
+ errors.length > 0,
79
+ name,
80
+ `should reject wrong type on field: ${sf} (sent number, expected string)`
81
+ );
82
+ }
83
+
84
+ // ── Test 3: Unknown field injection ──
85
+ const injected = { ...validArgs, userId: "injected", isAdmin: true, __proto__hack: "x" };
86
+ const { cleaned } = validateSchema(injected, schema);
87
+ assert(!("userId" in cleaned), name, "should strip injected 'userId'");
88
+ assert(!("isAdmin" in cleaned), name, "should strip injected 'isAdmin'");
89
+ assert(!("__proto__hack" in cleaned), name, "should strip injected '__proto__hack'");
90
+ }
91
+
92
+ // ── Summary ──
93
+ console.log("─".repeat(60));
94
+ console.log(`PASS: ${pass}`);
95
+ console.log(`FAIL: ${fail}`);
96
+ console.log(`TOOLS: ${toolNames.length}`);
97
+ console.log("─".repeat(60));
98
+
99
+ if (failures.length > 0) {
100
+ console.log("\nFAILURES:");
101
+ for (const f of failures) console.log(` ✗ ${f}`);
102
+ process.exit(1);
103
+ }
104
+
105
+ console.log("\nAll validation tests passed.");
package/tool-map.js CHANGED
@@ -1141,6 +1141,64 @@ const TOOL_MAP = {
1141
1141
  // Unblock path: fix RPC registry record:"scenes" → project:true
1142
1142
 
1143
1143
  // REMOVED: taskovergg_get_activity_feed — not in M0 matrix (no MCP tool in TOOLS array)
1144
+
1145
+ // ── Blueprint Intelligence ──
1146
+ taskovergg_sync_blueprint_data: {
1147
+ rpc: "syncDiscoveredBps",
1148
+ schema: {
1149
+ required: ["project_id", "schema_version", "blueprints"],
1150
+ fields: { project_id: "string", schema_version: "number", blueprints: "array" },
1151
+ },
1152
+ args: (c, r) => [r.project_id, r.blueprints, r.schema_version],
1153
+ },
1154
+ taskovergg_sync_debug: {
1155
+ rpc: "getSyncDebugInfo",
1156
+ schema: { required: ["project_id"], fields: { project_id: "string" } },
1157
+ args: (c, r) => [r.project_id],
1158
+ },
1159
+ taskovergg_get_discovered_bp_detail: {
1160
+ rpc: "getDiscoveredBpDetail",
1161
+ schema: { required: ["blueprint_id"], fields: { blueprint_id: "string", project_id: "string" } },
1162
+ args: (c, r) => [r.blueprint_id],
1163
+ },
1164
+ taskovergg_blueprint_complexity: {
1165
+ rpc: "computeBlueprintHealth",
1166
+ schema: { required: [], fields: { project_id: "string", blueprint_id: "string" } },
1167
+ args: (c, r) => [r.project_id],
1168
+ // Note: For single-blueprint complexity, use via local MCP mode. Cloud routes to project-level health.
1169
+ },
1170
+ taskovergg_blueprint_timeline: {
1171
+ rpc: "getBlueprintTimeline",
1172
+ schema: { required: ["blueprint_id"], fields: { blueprint_id: "string", limit: "number", include_layout: "boolean" } },
1173
+ args: (c, r) => [r.blueprint_id, c.limit, c.include_layout],
1174
+ },
1175
+ taskovergg_blueprint_summary: {
1176
+ rpc: "generateBlueprintSummary",
1177
+ schema: { required: ["blueprint_id"], fields: { blueprint_id: "string" } },
1178
+ args: (c, r) => [r.blueprint_id],
1179
+ },
1180
+ taskovergg_blueprint_review: {
1181
+ rpc: "computeProjectBlueprintReview",
1182
+ schema: { required: [], fields: { project_id: "string", blueprint_id: "string" } },
1183
+ args: (c, r) => [r.project_id],
1184
+ // Note: For single-blueprint review, use via local MCP mode. Cloud routes to project-level review.
1185
+ },
1186
+ taskovergg_blueprint_standards: {
1187
+ rpc: "getBlueprintStandards",
1188
+ schema: { required: ["project_id", "action"], fields: { project_id: "string", action: "string", rule_id: "string", enabled: "boolean" } },
1189
+ args: (c, r) => [r.project_id],
1190
+ // Note: toggle/evaluate actions handled via local MCP mode. Cloud routes to get-standards.
1191
+ },
1192
+ taskovergg_blueprint_impact: {
1193
+ rpc: "computeImpactAnalysis",
1194
+ schema: { required: ["project_id", "blueprint_id"], fields: { project_id: "string", blueprint_id: "string" } },
1195
+ args: (c, r) => [r.project_id, r.blueprint_id],
1196
+ },
1197
+ taskovergg_blueprint_dependencies: {
1198
+ rpc: "buildBlueprintDependencyGraph",
1199
+ schema: { required: ["project_id"], fields: { project_id: "string" } },
1200
+ args: (c, r) => [r.project_id],
1201
+ },
1144
1202
  };
1145
1203
 
1146
1204
  module.exports = { TOOL_MAP, validateSchema };
File without changes