@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.
Files changed (2) hide show
  1. package/index.js +375 -0
  2. 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
+ }