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.
- package/auth-flow.js +228 -0
- package/auth-gate.js +253 -0
- package/cloud-adapter.js +73 -26
- package/credential-store.js +93 -0
- package/crypto.js +386 -0
- package/data-store.js +9352 -0
- package/data-store.json-backup.js +1264 -0
- package/db.js +2292 -0
- package/image-moderator.js +491 -0
- package/image-processor.js +160 -0
- package/image-upload-service.js +398 -0
- package/index.js +2294 -2068
- package/migrate-json-to-sqlite.js +256 -0
- package/package.json +29 -16
- package/publish/auth-flow.js +275 -0
- package/publish/cloud-adapter.js +246 -0
- package/publish/credential-store.js +93 -0
- package/publish/index.js +1433 -0
- package/publish/package.json +21 -0
- package/publish/tool-map.js +1146 -0
- package/scripts/build-publish.sh +95 -0
- package/scripts/test-auth-failure.js +68 -0
- package/scripts/test-success.js +232 -0
- package/scripts/test-validation.js +105 -0
- package/tool-map.js +58 -0
- /package/{README.md → publish/README.md} +0 -0
|
@@ -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
|