taskover-mcp 1.0.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/README.md +26 -0
- package/cloud-adapter.js +222 -0
- package/index.js +2068 -0
- package/package.json +16 -0
- package/tool-map.js +1146 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# taskover-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [TaskOver.gg](https://taskover.gg) - connect Claude Desktop, Claude Code, Cursor, or Windsurf to your game dev project data.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Get your API key from taskover.gg -> Settings -> Security
|
|
8
|
+
2. Add to your AI host config:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"mcpServers": {
|
|
13
|
+
"taskover": {
|
|
14
|
+
"command": "npx",
|
|
15
|
+
"args": ["-y", "taskover-mcp@latest"],
|
|
16
|
+
"env": {
|
|
17
|
+
"TASKOVER_API_KEY": "tok_paste-your-key-here"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
3. Restart your AI host
|
|
25
|
+
|
|
26
|
+
Your API key is stored in plaintext in the config file. For better security, use Bridge Mode when available.
|
package/cloud-adapter.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// mcp-server/cloud-adapter.js
|
|
2
|
+
// SECURITY: This module is the cloud-mode replacement for direct data-store access.
|
|
3
|
+
// It forwards allowed methods to api.taskover.gg/api/rpc with the user's API key.
|
|
4
|
+
// The server is the final authority on auth, scope, ownership, and rate limits.
|
|
5
|
+
// Client-side allowlist is defense-in-depth only.
|
|
6
|
+
|
|
7
|
+
const API_BASE = "https://api.taskover.gg";
|
|
8
|
+
const REQUEST_TIMEOUT_MS = 15000;
|
|
9
|
+
const MAX_REQUEST_BYTES = 1 * 1024 * 1024; // 1 MB
|
|
10
|
+
const MAX_RESPONSE_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
11
|
+
|
|
12
|
+
// SECURITY: Default is DENY. New methods must be explicitly added here after security review.
|
|
13
|
+
// Delete operations are deferred past beta -- users must delete via the web UI.
|
|
14
|
+
// Settings, admin, billing, team, import/export are permanently blocked.
|
|
15
|
+
// REVISED per M0 matrix (33 read methods). Removed: getProject, getTask, getSystem,
|
|
16
|
+
// getMilestone, getLastSession, getBlueprintGraphs (DEFERRED), searchProject,
|
|
17
|
+
// getActivityFeed (no MCP tool). Singular get methods don't exist in RPC.
|
|
18
|
+
const MCP_ALLOWED_READS = new Set([
|
|
19
|
+
"listProjects",
|
|
20
|
+
"getTasks",
|
|
21
|
+
"getBugs",
|
|
22
|
+
"getSystems",
|
|
23
|
+
"getMilestones",
|
|
24
|
+
"getSessions",
|
|
25
|
+
"getChangelog",
|
|
26
|
+
"getDecisions",
|
|
27
|
+
"getBlueprints",
|
|
28
|
+
"getNotes",
|
|
29
|
+
"getBuildErrors",
|
|
30
|
+
"getAssets",
|
|
31
|
+
"getOptimizeItems",
|
|
32
|
+
"getPerfBudget",
|
|
33
|
+
"getLevels",
|
|
34
|
+
"getMarketingItems",
|
|
35
|
+
"getDialogues",
|
|
36
|
+
"getSounds", "getControls",
|
|
37
|
+
"getRefs",
|
|
38
|
+
"getPlugins",
|
|
39
|
+
"getShipChecked",
|
|
40
|
+
"getIterations",
|
|
41
|
+
"getPlaytests",
|
|
42
|
+
"getOpenQuestions",
|
|
43
|
+
"getWikiPages",
|
|
44
|
+
"getStories", "getScenes", "getSceneContent", "getStoryBible",
|
|
45
|
+
"search",
|
|
46
|
+
"dashboard", "contextExport",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// REVISED per M0 matrix (52 write methods). Removed: updateBug (no MCP tool),
|
|
50
|
+
// setBlueprintGraph, addGraphNodes (DEFERRED ANOMALY-1), addBoardColumn (DEFERRED ANOMALY-2),
|
|
51
|
+
// updateStoryCharacter, updateSceneContent (DEFERRED ANOMALY-3).
|
|
52
|
+
const MCP_ALLOWED_WRITES = new Set([
|
|
53
|
+
"addTask", "updateTask", "moveTask", "addTaskComment",
|
|
54
|
+
"addBug", "fixBug",
|
|
55
|
+
"logSession",
|
|
56
|
+
"addChangelog",
|
|
57
|
+
"logDecision",
|
|
58
|
+
"addSystem", "updateSystem",
|
|
59
|
+
"addBlueprint", "updateBlueprint",
|
|
60
|
+
"addNote",
|
|
61
|
+
"addBuildError", "fixBuildError",
|
|
62
|
+
"addOptimizeItem", "updateOptimizeItem",
|
|
63
|
+
"addPerfBudget",
|
|
64
|
+
"addLevel", "updateLevel", "addActor", "removeActor",
|
|
65
|
+
"addPlugin", "updatePlugin",
|
|
66
|
+
"addPlaytest",
|
|
67
|
+
"addMilestone", "updateMilestone",
|
|
68
|
+
"addIteration",
|
|
69
|
+
"addDialogue", "updateDialogue",
|
|
70
|
+
"addSound", "updateSound",
|
|
71
|
+
"addControl", "updateControl",
|
|
72
|
+
"addAsset", "updateAsset",
|
|
73
|
+
"addRef",
|
|
74
|
+
"addMarketingItem", "updateMarketingItem",
|
|
75
|
+
"addWikiPage", "updateWikiPage",
|
|
76
|
+
"addOpenQuestion", "toggleQuestion",
|
|
77
|
+
"toggleShipCheck",
|
|
78
|
+
"logBackup",
|
|
79
|
+
"addStory", "updateStory",
|
|
80
|
+
"addScene", "updateScene",
|
|
81
|
+
"updateStoryBible",
|
|
82
|
+
"addStoryCharacter",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
// SECURITY: Methods that are NEVER allowed through MCP, regardless of future changes.
|
|
86
|
+
// Commented for documentation -- they are blocked by not being in the allow sets above.
|
|
87
|
+
// addProject, deleteProject, updateProject
|
|
88
|
+
// generateApiKey, revokeApiKey
|
|
89
|
+
// any user/auth/team/billing/subscription methods
|
|
90
|
+
// exportData, importData
|
|
91
|
+
// any settings methods
|
|
92
|
+
// any friend/social methods
|
|
93
|
+
// loadAll (returns entire user dataset -- too broad)
|
|
94
|
+
// deleteSystem, deletePlugin, deleteScene (delete ops deferred past beta)
|
|
95
|
+
// DEFERRED per M0: getBlueprintGraphs, setBlueprintGraph, addGraphNodes (ANOMALY-1: ownership)
|
|
96
|
+
// DEFERRED per M0: addBoardColumn (ANOMALY-2: no RPC method)
|
|
97
|
+
// DEFERRED per M0: updateStoryCharacter, updateSceneContent (ANOMALY-3: arg/ownership)
|
|
98
|
+
// REMOVED (no MCP tool): updateBug, getProject, getTask, getSystem, getMilestone,
|
|
99
|
+
// getLastSession, searchProject, getActivityFeed
|
|
100
|
+
|
|
101
|
+
const MCP_ALLOWED_METHODS = new Set([...MCP_ALLOWED_READS, ...MCP_ALLOWED_WRITES]);
|
|
102
|
+
|
|
103
|
+
let _apiKey = null;
|
|
104
|
+
|
|
105
|
+
function init(apiKey) {
|
|
106
|
+
_apiKey = apiKey;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isAllowed(method) {
|
|
110
|
+
return MCP_ALLOWED_METHODS.has(method);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isWrite(method) {
|
|
114
|
+
return MCP_ALLOWED_WRITES.has(method);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function callRpc(method, args) {
|
|
118
|
+
if (!_apiKey) throw new Error("Cloud adapter not initialized -- no API key");
|
|
119
|
+
if (!isAllowed(method)) throw new Error(`Method "${method}" is not allowed through MCP`);
|
|
120
|
+
|
|
121
|
+
const body = JSON.stringify({ method, args: args || [] });
|
|
122
|
+
if (Buffer.byteLength(body, "utf8") > MAX_REQUEST_BYTES) {
|
|
123
|
+
throw new Error("Request too large (max 1 MB)");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch(`${API_BASE}/api/rpc`, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: {
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
"Authorization": `Bearer ${_apiKey}`,
|
|
135
|
+
},
|
|
136
|
+
body,
|
|
137
|
+
signal: controller.signal,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
|
|
142
|
+
if (res.status === 401) {
|
|
143
|
+
throw new Error("AUTH_FAILED");
|
|
144
|
+
}
|
|
145
|
+
if (res.status === 429) {
|
|
146
|
+
const retryAfter = res.headers.get("retry-after") || "60";
|
|
147
|
+
throw new Error(`RATE_LIMITED:${retryAfter}`);
|
|
148
|
+
}
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
const errBody = await res.text().catch(() => "");
|
|
151
|
+
throw new Error(`Cloud API error ${res.status}: ${errBody.slice(0, 200)}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// SECURITY: Enforce response size limit using byte-accurate check.
|
|
155
|
+
// text.length counts UTF-16 code units, not bytes. Multi-byte content
|
|
156
|
+
// (emoji, CJK, etc.) would undercount. Buffer.byteLength is correct.
|
|
157
|
+
const text = await res.text();
|
|
158
|
+
if (Buffer.byteLength(text, "utf8") > MAX_RESPONSE_BYTES) {
|
|
159
|
+
throw new Error("Response too large (max 5 MB)");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const parsed = JSON.parse(text);
|
|
163
|
+
return parsed.result !== undefined ? parsed.result : parsed;
|
|
164
|
+
} catch (err) {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
if (err.name === "AbortError") {
|
|
167
|
+
throw new Error("Cloud API request timed out (15s)");
|
|
168
|
+
}
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function validateKey() {
|
|
174
|
+
// SECURITY: Validate auth via dedicated lightweight endpoint.
|
|
175
|
+
// /api/auth/validate confirms the key is valid and returns userId + displayName.
|
|
176
|
+
// Does NOT load business data (unlike listProjects which fetches all projects).
|
|
177
|
+
// /api/health is unauthenticated and must NEVER be used for key validation.
|
|
178
|
+
if (!_apiKey) return { valid: false, error: "No API key configured" };
|
|
179
|
+
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const res = await fetch(`${API_BASE}/api/auth/validate`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: {
|
|
187
|
+
"Content-Type": "application/json",
|
|
188
|
+
"Authorization": `Bearer ${_apiKey}`,
|
|
189
|
+
},
|
|
190
|
+
signal: controller.signal,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
|
|
195
|
+
if (res.status === 401) {
|
|
196
|
+
return { valid: false, error: "API key is invalid or expired. Generate a new key at taskover.gg -> Settings -> Security." };
|
|
197
|
+
}
|
|
198
|
+
if (!res.ok) {
|
|
199
|
+
return { valid: false, error: `Unexpected response from api.taskover.gg (HTTP ${res.status})` };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const data = await res.json();
|
|
203
|
+
return { valid: true, displayName: data.displayName || "user" };
|
|
204
|
+
} catch (err) {
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
if (err.name === "AbortError") {
|
|
207
|
+
return { valid: false, error: "Cannot reach api.taskover.gg (timeout). Check your internet connection." };
|
|
208
|
+
}
|
|
209
|
+
return { valid: false, error: `Cannot reach api.taskover.gg: ${err.message}` };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
init,
|
|
215
|
+
isAllowed,
|
|
216
|
+
isWrite,
|
|
217
|
+
callRpc,
|
|
218
|
+
validateKey,
|
|
219
|
+
MCP_ALLOWED_METHODS,
|
|
220
|
+
MCP_ALLOWED_READS,
|
|
221
|
+
MCP_ALLOWED_WRITES,
|
|
222
|
+
};
|