Skip to content

Commit 61bd0e9

Browse files
hyperpolymathclaude
andcommitted
feat(cartridges): implement mod.js for 7 unimplemented cartridges
All 7 previously stub-free cartridges now have full mod.js implementations following the estate handleTool pattern. Also adds cartridge.json for model-router-mcp which had none. - claude-agents-power-mcp: GITHUB_TOKEN bearer auth, 5 tools, port 3000 - coderag-mcp: no auth, Neo4j-backed graph analysis, 5 tools, port 7474 - local-memory-mcp: no auth, SQLite-backed, 13 tools, port 7750 - model-router-mcp: pure local logic (no backend), 4 tools — classify/ plan/review/estimate re-implemented from src/router.js - notifyhub-mcp: NOTIFYHUB_API_KEY auth, 5 channel tools, port 8080 - opendatamcp: no auth, public dataset access/publish, 5 tools, port 8000 - origenemcp: ORIGENE_API_KEY auth, biomedical DBs, 5 tools, port 8788 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 35174bb commit 61bd0e9

8 files changed

Lines changed: 824 additions & 0 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// claude-agents-power-mcp/mod.js — Claude Agents Power MCP cartridge.
5+
//
6+
// Delegates to backend at http://127.0.0.1:3000 (override with CLAUDE_AGENTS_URL).
7+
// Auth: GITHUB_TOKEN (required for agent install; list/search work without it).
8+
9+
const BASE_URL = Deno.env.get("CLAUDE_AGENTS_URL") ?? "http://127.0.0.1:3000";
10+
const TIMEOUT_MS = 20_000;
11+
12+
function getToken() {
13+
return Deno.env.get("GITHUB_TOKEN") ?? null;
14+
}
15+
16+
function authHeaders() {
17+
const token = getToken();
18+
const h = { "Content-Type": "application/json" };
19+
if (token) h["Authorization"] = `Bearer ${token}`;
20+
return h;
21+
}
22+
23+
async function post(path, payload) {
24+
const ctrl = new AbortController();
25+
const t = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
26+
try {
27+
const r = await fetch(`${BASE_URL}${path}`, {
28+
method: "POST",
29+
headers: authHeaders(),
30+
body: JSON.stringify(payload),
31+
signal: ctrl.signal,
32+
});
33+
const data = await r.json().catch(() => ({ success: false, error: "non-JSON response" }));
34+
return { status: r.status, data };
35+
} catch (e) {
36+
if (e.name === "AbortError")
37+
return { status: 504, data: { success: false, error: "claude-agents-power-mcp backend timed out" } };
38+
return { status: 503, data: { success: false, error: `claude-agents-power-mcp backend unavailable: ${e.message}` } };
39+
} finally { clearTimeout(t); }
40+
}
41+
42+
export async function handleTool(toolName, args) {
43+
switch (toolName) {
44+
case "claude_agents_analyze_project": {
45+
const { project_path, language } = args ?? {};
46+
const payload = {};
47+
if (project_path !== undefined) payload.project_path = project_path;
48+
if (language !== undefined) payload.language = language;
49+
return post("/api/v1/agents/analyze", payload);
50+
}
51+
52+
case "claude_agents_list_agents": {
53+
const { category, language, limit } = args ?? {};
54+
const payload = {};
55+
if (category !== undefined) payload.category = category;
56+
if (language !== undefined) payload.language = language;
57+
if (limit !== undefined) payload.limit = limit;
58+
return post("/api/v1/agents/list", payload);
59+
}
60+
61+
case "claude_agents_search_agents": {
62+
const { keywords, language } = args ?? {};
63+
if (!keywords) return { status: 400, data: { error: "keywords is required" } };
64+
const payload = { keywords };
65+
if (language !== undefined) payload.language = language;
66+
return post("/api/v1/agents/search", payload);
67+
}
68+
69+
case "claude_agents_install_agents": {
70+
if (!getToken())
71+
return { status: 401, data: { error: "GITHUB_TOKEN env var is required to install agents" } };
72+
const { agent_ids, target_dir, language } = args ?? {};
73+
if (!agent_ids || !Array.isArray(agent_ids) || agent_ids.length === 0)
74+
return { status: 400, data: { error: "agent_ids array is required" } };
75+
const payload = { agent_ids };
76+
if (target_dir !== undefined) payload.target_dir = target_dir;
77+
if (language !== undefined) payload.language = language;
78+
return post("/api/v1/agents/install", payload);
79+
}
80+
81+
case "claude_agents_get_download_stats": {
82+
const { agent_id, limit } = args ?? {};
83+
const payload = {};
84+
if (agent_id !== undefined) payload.agent_id = agent_id;
85+
if (limit !== undefined) payload.limit = limit;
86+
return post("/api/v1/agents/stats", payload);
87+
}
88+
89+
default:
90+
return { status: 404, data: { error: `Unknown tool: ${toolName}` } };
91+
}
92+
}

cartridges/coderag-mcp/mod.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// coderag-mcp/mod.js — CodeRAG enterprise code intelligence cartridge.
5+
//
6+
// Delegates to backend at http://127.0.0.1:7474 (override with CODERAG_URL).
7+
// No auth required. The backend connects to Neo4j on bolt://127.0.0.1:7687.
8+
9+
const BASE_URL = Deno.env.get("CODERAG_URL") ?? "http://127.0.0.1:7474";
10+
const TIMEOUT_MS = 60_000; // graph analysis can be slow
11+
12+
async function post(path, payload) {
13+
const ctrl = new AbortController();
14+
const t = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
15+
try {
16+
const r = await fetch(`${BASE_URL}${path}`, {
17+
method: "POST",
18+
headers: { "Content-Type": "application/json" },
19+
body: JSON.stringify(payload),
20+
signal: ctrl.signal,
21+
});
22+
const data = await r.json().catch(() => ({ success: false, error: "non-JSON response" }));
23+
return { status: r.status, data };
24+
} catch (e) {
25+
if (e.name === "AbortError")
26+
return { status: 504, data: { success: false, error: "coderag-mcp backend timed out" } };
27+
return { status: 503, data: { success: false, error: `coderag-mcp backend unavailable: ${e.message}` } };
28+
} finally { clearTimeout(t); }
29+
}
30+
31+
export async function handleTool(toolName, args) {
32+
switch (toolName) {
33+
case "coderag_analyze_repository": {
34+
const { repository_url, branch, auth_token } = args ?? {};
35+
if (!repository_url) return { status: 400, data: { error: "repository_url is required" } };
36+
const payload = { repository_url };
37+
if (branch !== undefined) payload.branch = branch;
38+
if (auth_token !== undefined) payload.auth_token = auth_token;
39+
return post("/api/v1/analyze", payload);
40+
}
41+
42+
case "coderag_query_knowledge_graph": {
43+
const { query, language } = args ?? {};
44+
if (!query) return { status: 400, data: { error: "query is required" } };
45+
const payload = { query };
46+
if (language !== undefined) payload.language = language;
47+
return post("/api/v1/query", payload);
48+
}
49+
50+
case "coderag_calculate_metrics": {
51+
const { repository_url, metric_types } = args ?? {};
52+
if (!repository_url) return { status: 400, data: { error: "repository_url is required" } };
53+
const payload = { repository_url };
54+
if (metric_types !== undefined) payload.metric_types = metric_types;
55+
return post("/api/v1/metrics", payload);
56+
}
57+
58+
case "coderag_semantic_search": {
59+
const { query, repository_url } = args ?? {};
60+
if (!query) return { status: 400, data: { error: "query is required" } };
61+
const payload = { query };
62+
if (repository_url !== undefined) payload.repository_url = repository_url;
63+
return post("/api/v1/search", payload);
64+
}
65+
66+
case "coderag_detect_language": {
67+
const { repository_url } = args ?? {};
68+
if (!repository_url) return { status: 400, data: { error: "repository_url is required" } };
69+
return post("/api/v1/detect-language", { repository_url });
70+
}
71+
72+
default:
73+
return { status: 404, data: { error: `Unknown tool: ${toolName}` } };
74+
}
75+
}

cartridges/local-memory-mcp/mod.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// local-memory-mcp/mod.js — Persistent local memory cartridge (SQLite-backed).
5+
//
6+
// Delegates to backend at http://127.0.0.1:7750 (override with LOCAL_MEMORY_URL).
7+
// No auth required. All data stays local — no cloud, no API keys.
8+
9+
const BASE_URL = Deno.env.get("LOCAL_MEMORY_URL") ?? "http://127.0.0.1:7750";
10+
const TIMEOUT_MS = 15_000;
11+
12+
async function post(path, payload) {
13+
const ctrl = new AbortController();
14+
const t = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
15+
try {
16+
const r = await fetch(`${BASE_URL}${path}`, {
17+
method: "POST",
18+
headers: { "Content-Type": "application/json" },
19+
body: JSON.stringify(payload),
20+
signal: ctrl.signal,
21+
});
22+
const data = await r.json().catch(() => ({ success: false, error: "non-JSON response" }));
23+
return { status: r.status, data };
24+
} catch (e) {
25+
if (e.name === "AbortError")
26+
return { status: 504, data: { success: false, error: "local-memory-mcp backend timed out" } };
27+
return { status: 503, data: { success: false, error: `local-memory-mcp backend unavailable: ${e.message}` } };
28+
} finally { clearTimeout(t); }
29+
}
30+
31+
export async function handleTool(toolName, args) {
32+
switch (toolName) {
33+
case "memory_session_start": {
34+
const { project } = args ?? {};
35+
const payload = {};
36+
if (project !== undefined) payload.project = project;
37+
return post("/api/v1/session/start", payload);
38+
}
39+
40+
case "memory_session_end": {
41+
const { summary } = args ?? {};
42+
const payload = {};
43+
if (summary !== undefined) payload.summary = summary;
44+
return post("/api/v1/session/end", payload);
45+
}
46+
47+
case "memory_learn": {
48+
const { category, content, tags, confidence, project } = args ?? {};
49+
if (!category || !content)
50+
return { status: 400, data: { error: "category and content are required" } };
51+
const payload = { category, content };
52+
if (tags !== undefined) payload.tags = tags;
53+
if (confidence !== undefined) payload.confidence = confidence;
54+
if (project !== undefined) payload.project = project;
55+
return post("/api/v1/learnings", payload);
56+
}
57+
58+
case "memory_recall": {
59+
const { query, limit } = args ?? {};
60+
const payload = {};
61+
if (query !== undefined) payload.query = query;
62+
if (limit !== undefined) payload.limit = limit;
63+
return post("/api/v1/learnings/recall", payload);
64+
}
65+
66+
case "memory_search": {
67+
const { query, types, limit } = args ?? {};
68+
if (!query) return { status: 400, data: { error: "query is required" } };
69+
const payload = { query };
70+
if (types !== undefined) payload.types = types;
71+
if (limit !== undefined) payload.limit = limit;
72+
return post("/api/v1/search", payload);
73+
}
74+
75+
case "memory_decide": {
76+
const { title, decision, reasoning, alternatives, confidence, project } = args ?? {};
77+
if (!title || !decision || !reasoning)
78+
return { status: 400, data: { error: "title, decision, and reasoning are required" } };
79+
const payload = { title, decision, reasoning };
80+
if (alternatives !== undefined) payload.alternatives = alternatives;
81+
if (confidence !== undefined) payload.confidence = confidence;
82+
if (project !== undefined) payload.project = project;
83+
return post("/api/v1/decisions", payload);
84+
}
85+
86+
case "memory_entity_observe": {
87+
const { entityName, entityType, content } = args ?? {};
88+
if (!entityName || !entityType || !content)
89+
return { status: 400, data: { error: "entityName, entityType, and content are required" } };
90+
return post("/api/v1/entities/observe", { entityName, entityType, content });
91+
}
92+
93+
case "memory_entity_search": {
94+
const { query, entityType, limit } = args ?? {};
95+
if (!query) return { status: 400, data: { error: "query is required" } };
96+
const payload = { query };
97+
if (entityType !== undefined) payload.entityType = entityType;
98+
if (limit !== undefined) payload.limit = limit;
99+
return post("/api/v1/entities/search", payload);
100+
}
101+
102+
case "memory_entity_open": {
103+
const { name, id } = args ?? {};
104+
const payload = {};
105+
if (name !== undefined) payload.name = name;
106+
if (id !== undefined) payload.id = id;
107+
return post("/api/v1/entities/open", payload);
108+
}
109+
110+
case "memory_entity_relate": {
111+
const { fromEntityId, toEntityId, relationType, weight } = args ?? {};
112+
if (!fromEntityId || !toEntityId || !relationType)
113+
return { status: 400, data: { error: "fromEntityId, toEntityId, and relationType are required" } };
114+
const payload = { fromEntityId, toEntityId, relationType };
115+
if (weight !== undefined) payload.weight = weight;
116+
return post("/api/v1/entities/relate", payload);
117+
}
118+
119+
case "memory_insights": {
120+
const { project } = args ?? {};
121+
const payload = {};
122+
if (project !== undefined) payload.project = project;
123+
return post("/api/v1/insights", payload);
124+
}
125+
126+
case "memory_profile_set": {
127+
const { field, value } = args ?? {};
128+
if (!field || !value) return { status: 400, data: { error: "field and value are required" } };
129+
return post("/api/v1/profile/set", { field, value });
130+
}
131+
132+
case "memory_profile_get": {
133+
const { field } = args ?? {};
134+
if (!field) return { status: 400, data: { error: "field is required" } };
135+
return post("/api/v1/profile/get", { field });
136+
}
137+
138+
default:
139+
return { status: 404, data: { error: `Unknown tool: ${toolName}` } };
140+
}
141+
}

0 commit comments

Comments
 (0)