@projectjade/mcp-server 1.3.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/index.js +375 -0
- package/package.json +31 -0
package/index.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* JadeGate MCP Server
|
|
4
|
+
* Exposes jade-core verification, search, and info capabilities via MCP protocol.
|
|
5
|
+
*
|
|
6
|
+
* Tools:
|
|
7
|
+
* jade_verify — Verify a skill JSON (pass raw JSON or file path)
|
|
8
|
+
* jade_search — Search skills by keyword/tag
|
|
9
|
+
* jade_info — Get detailed info about a skill
|
|
10
|
+
* jade_list — List all available skills
|
|
11
|
+
* jade_stats — Registry statistics
|
|
12
|
+
* jade_dag — Visualize skill execution DAG (Mermaid/D3/DOT)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import {
|
|
18
|
+
CallToolRequestSchema,
|
|
19
|
+
ListToolsRequestSchema,
|
|
20
|
+
ListResourcesRequestSchema,
|
|
21
|
+
ReadResourceRequestSchema,
|
|
22
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
import { execSync } from "child_process";
|
|
24
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
25
|
+
import { join, resolve } from "path";
|
|
26
|
+
|
|
27
|
+
// Find jade-core skill directory
|
|
28
|
+
const SKILL_DIRS = [
|
|
29
|
+
join(process.cwd(), "jade_skills"),
|
|
30
|
+
join(process.env.HOME || "", ".jadegate", "skills"),
|
|
31
|
+
"/usr/local/share/jadegate/skills",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function findSkillDir() {
|
|
35
|
+
for (const dir of SKILL_DIRS) {
|
|
36
|
+
if (existsSync(dir)) return dir;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runJadeCmd(args) {
|
|
42
|
+
try {
|
|
43
|
+
const result = execSync(`python3 -c "
|
|
44
|
+
import sys, json
|
|
45
|
+
sys.path.insert(0, '.')
|
|
46
|
+
${args}
|
|
47
|
+
"`, { cwd: findSkillDir()?.replace("/jade_skills", "") || ".", timeout: 30000, encoding: "utf-8" });
|
|
48
|
+
return result.trim();
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return `Error: ${e.message}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadSkillIndex() {
|
|
55
|
+
const skillDir = findSkillDir();
|
|
56
|
+
if (!skillDir) return [];
|
|
57
|
+
const baseDir = skillDir.replace("/jade_skills", "");
|
|
58
|
+
const indexPath = join(baseDir, "jade_registry", "skill_index.json");
|
|
59
|
+
if (existsSync(indexPath)) {
|
|
60
|
+
return JSON.parse(readFileSync(indexPath, "utf-8")).skills || [];
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getAllSkills() {
|
|
66
|
+
const skillDir = findSkillDir();
|
|
67
|
+
if (!skillDir) return [];
|
|
68
|
+
const skills = [];
|
|
69
|
+
for (const sub of ["mcp", "tools", ""]) {
|
|
70
|
+
const dir = sub ? join(skillDir, sub) : skillDir;
|
|
71
|
+
if (!existsSync(dir)) continue;
|
|
72
|
+
for (const f of readdirSync(dir)) {
|
|
73
|
+
if (f.endsWith(".json") && !f.includes(".sig.")) {
|
|
74
|
+
try {
|
|
75
|
+
const data = JSON.parse(readFileSync(join(dir, f), "utf-8"));
|
|
76
|
+
skills.push({ file: join(dir, f), ...data });
|
|
77
|
+
} catch {}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return skills;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const server = new Server(
|
|
85
|
+
{ name: "jadegate", version: "1.3.0" },
|
|
86
|
+
{ capabilities: { tools: {}, resources: {} } }
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// === TOOLS ===
|
|
90
|
+
|
|
91
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
92
|
+
tools: [
|
|
93
|
+
{
|
|
94
|
+
name: "jade_verify",
|
|
95
|
+
description: "Verify a JadeGate skill JSON. Returns 5-layer validation results with pass/fail status and confidence score.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
skill_json: { type: "string", description: "Raw JSON string of the skill to verify" },
|
|
100
|
+
file_path: { type: "string", description: "Path to a skill JSON file to verify" },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "jade_search",
|
|
106
|
+
description: "Search JadeGate skill registry by keyword, tag, or category.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
query: { type: "string", description: "Search query (keyword or tag)" },
|
|
111
|
+
category: { type: "string", enum: ["mcp", "tools", "standalone"], description: "Filter by category" },
|
|
112
|
+
limit: { type: "number", description: "Max results (default 10)" },
|
|
113
|
+
},
|
|
114
|
+
required: ["query"],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "jade_info",
|
|
119
|
+
description: "Get detailed information about a specific JadeGate skill by ID.",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
skill_id: { type: "string", description: "The skill ID to look up" },
|
|
124
|
+
},
|
|
125
|
+
required: ["skill_id"],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "jade_list",
|
|
130
|
+
description: "List all available JadeGate skills with optional category filter.",
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
category: { type: "string", enum: ["mcp", "tools", "standalone"], description: "Filter by category" },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "jade_stats",
|
|
140
|
+
description: "Get JadeGate registry statistics: total skills, categories, verification status.",
|
|
141
|
+
inputSchema: { type: "object", properties: {} },
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "jade_dag",
|
|
145
|
+
description: "Visualize a skill's execution DAG as Mermaid flowchart. Helps agents understand skill execution flow.",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
skill_id: { type: "string", description: "Skill ID to look up from registry" },
|
|
150
|
+
skill_json: { type: "string", description: "Raw skill JSON string" },
|
|
151
|
+
format: { type: "string", enum: ["mermaid", "d3", "dot"], description: "Output format (default: mermaid)" },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
159
|
+
const { name, arguments: args } = request.params;
|
|
160
|
+
|
|
161
|
+
switch (name) {
|
|
162
|
+
case "jade_verify": {
|
|
163
|
+
let filePath = args?.file_path;
|
|
164
|
+
let tempFile = null;
|
|
165
|
+
|
|
166
|
+
if (args?.skill_json && !filePath) {
|
|
167
|
+
const tmp = `/tmp/jade_verify_${Date.now()}.json`;
|
|
168
|
+
require("fs").writeFileSync(tmp, args.skill_json);
|
|
169
|
+
filePath = tmp;
|
|
170
|
+
tempFile = tmp;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!filePath) {
|
|
174
|
+
return { content: [{ type: "text", text: "Error: provide skill_json or file_path" }] };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const result = runJadeCmd(`
|
|
178
|
+
from jade_core.validator import JadeValidator
|
|
179
|
+
import json
|
|
180
|
+
v = JadeValidator()
|
|
181
|
+
// Sanitize file path
|
|
182
|
+
fp = "${filePath}".replace('"', '').replace("'", "").replace(";", "").replace("\\", "")
|
|
183
|
+
import os.path
|
|
184
|
+
fp = os.path.abspath(fp)
|
|
185
|
+
r = v.validate_file(fp)
|
|
186
|
+
out = {
|
|
187
|
+
"valid": r.valid,
|
|
188
|
+
"layers_passed": r.layers_passed,
|
|
189
|
+
"total_layers": r.total_layers,
|
|
190
|
+
"confidence": round(r.confidence, 4) if hasattr(r, 'confidence') else None,
|
|
191
|
+
"issues": [{"severity": i.severity.value if hasattr(i.severity, 'value') else str(i.severity), "layer": i.layer, "message": i.message} for i in r.issues]
|
|
192
|
+
}
|
|
193
|
+
print(json.dumps(out, indent=2))
|
|
194
|
+
`);
|
|
195
|
+
|
|
196
|
+
if (tempFile) try { require("fs").unlinkSync(tempFile); } catch {}
|
|
197
|
+
|
|
198
|
+
return { content: [{ type: "text", text: result }] };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case "jade_search": {
|
|
202
|
+
const query = (args?.query || "").toLowerCase();
|
|
203
|
+
const category = args?.category;
|
|
204
|
+
const limit = args?.limit || 10;
|
|
205
|
+
const skills = loadSkillIndex();
|
|
206
|
+
const matches = skills.filter(s => {
|
|
207
|
+
if (category && s.category !== category) return false;
|
|
208
|
+
const text = `${s.skill_id} ${s.name} ${s.description} ${(s.tags || []).join(" ")}`.toLowerCase();
|
|
209
|
+
return text.includes(query);
|
|
210
|
+
}).slice(0, limit);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
content: [{
|
|
214
|
+
type: "text",
|
|
215
|
+
text: JSON.stringify({ total: matches.length, results: matches.map(s => ({
|
|
216
|
+
skill_id: s.skill_id, name: s.name, description: s.description,
|
|
217
|
+
category: s.category, tags: s.tags, signed: s.signed
|
|
218
|
+
})) }, null, 2)
|
|
219
|
+
}]
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case "jade_info": {
|
|
224
|
+
const sid = args?.skill_id;
|
|
225
|
+
const skills = getAllSkills();
|
|
226
|
+
const skill = skills.find(s => s.skill_id === sid);
|
|
227
|
+
if (!skill) return { content: [{ type: "text", text: `Skill '${sid}' not found` }] };
|
|
228
|
+
const { file, ...data } = skill;
|
|
229
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case "jade_list": {
|
|
233
|
+
const category = args?.category;
|
|
234
|
+
const skills = loadSkillIndex();
|
|
235
|
+
const filtered = category ? skills.filter(s => s.category === category) : skills;
|
|
236
|
+
return {
|
|
237
|
+
content: [{
|
|
238
|
+
type: "text",
|
|
239
|
+
text: JSON.stringify({
|
|
240
|
+
total: filtered.length,
|
|
241
|
+
skills: filtered.map(s => ({ skill_id: s.skill_id, name: s.name, category: s.category }))
|
|
242
|
+
}, null, 2)
|
|
243
|
+
}]
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
case "jade_stats": {
|
|
248
|
+
const skills = loadSkillIndex();
|
|
249
|
+
const cats = {};
|
|
250
|
+
let signed = 0;
|
|
251
|
+
skills.forEach(s => {
|
|
252
|
+
cats[s.category] = (cats[s.category] || 0) + 1;
|
|
253
|
+
if (s.signed) signed++;
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
content: [{
|
|
257
|
+
type: "text",
|
|
258
|
+
text: JSON.stringify({
|
|
259
|
+
total_skills: skills.length,
|
|
260
|
+
signed_skills: signed,
|
|
261
|
+
categories: cats,
|
|
262
|
+
protocol_version: "1.0.0",
|
|
263
|
+
verification_layers: 5,
|
|
264
|
+
crypto: "Ed25519"
|
|
265
|
+
}, null, 2)
|
|
266
|
+
}]
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
case "jade_dag": {
|
|
271
|
+
let skillJson = args?.skill_json;
|
|
272
|
+
|
|
273
|
+
// If skill_id provided, find the file
|
|
274
|
+
if (args?.skill_id && !skillJson) {
|
|
275
|
+
const skills = getAllSkills();
|
|
276
|
+
const found = skills.find(s => s.skill_id === args.skill_id);
|
|
277
|
+
if (!found) {
|
|
278
|
+
return { content: [{ type: "text", text: `Skill not found: ${args.skill_id}` }] };
|
|
279
|
+
}
|
|
280
|
+
skillJson = JSON.stringify(found);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!skillJson) {
|
|
284
|
+
return { content: [{ type: "text", text: "Error: provide skill_id or skill_json" }] };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const fmt = args?.format || "mermaid";
|
|
288
|
+
const pyMethod = fmt === "d3" ? "to_d3_json" : fmt === "dot" ? "to_dot" : "to_mermaid";
|
|
289
|
+
const serialize = fmt === "d3" ? "import json; print(json.dumps(result, indent=2))" : "print(result)";
|
|
290
|
+
|
|
291
|
+
// Write skill JSON to temp file for safe passing
|
|
292
|
+
const tmpSkill = `/tmp/jade_dag_${Date.now()}.json`;
|
|
293
|
+
require("fs").writeFileSync(tmpSkill, skillJson);
|
|
294
|
+
|
|
295
|
+
const result = runJadeCmd(`
|
|
296
|
+
import json
|
|
297
|
+
from jade_core.models import JadeSkill
|
|
298
|
+
from jade_core.dag import DAGAnalyzer
|
|
299
|
+
with open('${tmpSkill}') as f:
|
|
300
|
+
data = json.load(f)
|
|
301
|
+
skill = JadeSkill.from_dict(data)
|
|
302
|
+
analyzer = DAGAnalyzer()
|
|
303
|
+
result = analyzer.${pyMethod}(skill)
|
|
304
|
+
${serialize}
|
|
305
|
+
`);
|
|
306
|
+
|
|
307
|
+
try { require("fs").unlinkSync(tmpSkill); } catch {}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
content: [{
|
|
311
|
+
type: "text",
|
|
312
|
+
text: result
|
|
313
|
+
}]
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
default:
|
|
318
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// === RESOURCES ===
|
|
323
|
+
|
|
324
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
325
|
+
resources: [
|
|
326
|
+
{
|
|
327
|
+
uri: "jadegate://registry/index",
|
|
328
|
+
name: "JadeGate Skill Registry",
|
|
329
|
+
description: "Complete index of all verified JadeGate skills",
|
|
330
|
+
mimeType: "application/json",
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
uri: "jadegate://trust/root-ca",
|
|
334
|
+
name: "JadeGate Root CA",
|
|
335
|
+
description: "Root Certificate Authority public key and trust anchors",
|
|
336
|
+
mimeType: "application/json",
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
342
|
+
const { uri } = request.params;
|
|
343
|
+
|
|
344
|
+
if (uri === "jadegate://registry/index") {
|
|
345
|
+
const skills = loadSkillIndex();
|
|
346
|
+
return {
|
|
347
|
+
contents: [{
|
|
348
|
+
uri,
|
|
349
|
+
mimeType: "application/json",
|
|
350
|
+
text: JSON.stringify({ total: skills.length, skills }, null, 2),
|
|
351
|
+
}],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (uri === "jadegate://trust/root-ca") {
|
|
356
|
+
const baseDir = findSkillDir()?.replace("/jade_skills", "");
|
|
357
|
+
const caPath = baseDir ? join(baseDir, "jadegate.pub.json") : null;
|
|
358
|
+
if (caPath && existsSync(caPath)) {
|
|
359
|
+
return { contents: [{ uri, mimeType: "application/json", text: readFileSync(caPath, "utf-8") }] };
|
|
360
|
+
}
|
|
361
|
+
return { contents: [{ uri, mimeType: "text/plain", text: "Root CA not found" }] };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// === START ===
|
|
368
|
+
|
|
369
|
+
async function main() {
|
|
370
|
+
const transport = new StdioServerTransport();
|
|
371
|
+
await server.connect(transport);
|
|
372
|
+
console.error("JadeGate MCP Server v1.3.0 running on stdio");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@projectjade/mcp-server",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "JadeGate MCP Server — Deterministic security verification for AI agent skills",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jadegate-mcp": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"mcp",
|
|
11
|
+
"jadegate",
|
|
12
|
+
"security",
|
|
13
|
+
"ai-agents",
|
|
14
|
+
"verification",
|
|
15
|
+
"skills"
|
|
16
|
+
],
|
|
17
|
+
"author": "JadeGate",
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/JadeGate/jade-core"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://jadegate.io",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
29
|
+
},
|
|
30
|
+
"type": "module"
|
|
31
|
+
}
|