@agent-root/mcp-server 0.1.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/bin/server.js +257 -0
- package/package.json +38 -0
package/bin/server.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// AgentRoot MCP Server — search and install AI agent skills via MCP
|
|
4
|
+
// Stateless, no auth, no config. All state lives in AgentRoot's API and on the local filesystem.
|
|
5
|
+
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const {
|
|
14
|
+
API_BASE, MANIFEST_FILE, TOOL_NAMES,
|
|
15
|
+
detectTools, resolveToolDir, hashContent, scanInstalled, writeSkill,
|
|
16
|
+
} = require('@agent-root/core');
|
|
17
|
+
|
|
18
|
+
const USER_AGENT = '@agent-root/mcp-server/0.1.0';
|
|
19
|
+
|
|
20
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
async function apiFetch(urlPath) {
|
|
23
|
+
const url = `${API_BASE}${urlPath}`;
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
headers: {
|
|
26
|
+
'Accept': 'application/json',
|
|
27
|
+
'User-Agent': USER_AGENT,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`API error: HTTP ${res.status} for ${url}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const contentType = res.headers.get('content-type') || '';
|
|
36
|
+
if (!contentType.includes('application/json')) {
|
|
37
|
+
throw new Error(`Expected JSON response from ${url}, got ${contentType}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return res.json();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchText(url) {
|
|
44
|
+
const res = await fetch(url, {
|
|
45
|
+
headers: { 'User-Agent': USER_AGENT },
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
49
|
+
}
|
|
50
|
+
return res.text();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── MCP Server ────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const server = new McpServer({
|
|
56
|
+
name: 'agentroot',
|
|
57
|
+
version: '0.1.0',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ─── Tool: search_skills ──────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
server.tool(
|
|
63
|
+
'search_skills',
|
|
64
|
+
'Search the AgentRoot registry for AI agent skills. Returns matching skills with name, description, domain, and verification status.',
|
|
65
|
+
{
|
|
66
|
+
query: z.string().describe('Search query (e.g. "stripe payments", "github actions", "database migrations")'),
|
|
67
|
+
verified_only: z.boolean().optional().describe('If true, only return skills verified via DNS by the domain owner'),
|
|
68
|
+
},
|
|
69
|
+
async ({ query, verified_only }) => {
|
|
70
|
+
try {
|
|
71
|
+
const data = await apiFetch(`/api/find-skills?q=${encodeURIComponent(query)}`);
|
|
72
|
+
|
|
73
|
+
if (!data.skills || data.skills.length === 0) {
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: 'text', text: `No skills found for "${query}".` }],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let skills = data.skills.map(s => ({
|
|
80
|
+
skill_id: s.skill_id,
|
|
81
|
+
name: s.name,
|
|
82
|
+
description: s.description,
|
|
83
|
+
domain: s.domain,
|
|
84
|
+
verified: s.source === 'dns',
|
|
85
|
+
skill_md_url: s.skill_md_url,
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
if (verified_only) {
|
|
89
|
+
skills = skills.filter(s => s.verified);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (skills.length === 0) {
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: 'text', text: `No verified skills found for "${query}". Try searching without verified_only filter.` }],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text', text: JSON.stringify(skills, null, 2) }],
|
|
100
|
+
};
|
|
101
|
+
} catch (err) {
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: 'text', text: `Error searching skills: ${err.message}` }],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// ─── Tool: install_skill ──────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
server.tool(
|
|
113
|
+
'install_skill',
|
|
114
|
+
'Install a skill from the AgentRoot registry. Fetches the SKILL.md from the publisher\'s domain and writes it to the correct tool directory. For unverified skills, a warning is included that should be presented to the user before confirming.',
|
|
115
|
+
{
|
|
116
|
+
domain: z.string().describe('The publisher domain (e.g. "stripe.com", "agentroot.io")'),
|
|
117
|
+
skill_id: z.string().describe('The skill identifier (e.g. "payments", "discover-skills")'),
|
|
118
|
+
tool: z.enum(['claude', 'codex', 'gemini', 'cursor', 'agents']).optional().describe('Target tool directory. Auto-detected if omitted.'),
|
|
119
|
+
},
|
|
120
|
+
async ({ domain, skill_id, tool }) => {
|
|
121
|
+
try {
|
|
122
|
+
// 1. Resolve skill metadata from API
|
|
123
|
+
const data = await apiFetch(`/api/skills/${encodeURIComponent(domain)}/item/${encodeURIComponent(skill_id)}`);
|
|
124
|
+
|
|
125
|
+
if (!data.skill) {
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: 'text', text: `Skill not found: ${domain}/${skill_id}` }],
|
|
128
|
+
isError: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const skill = data.skill;
|
|
133
|
+
const verified = skill.source === 'dns';
|
|
134
|
+
|
|
135
|
+
if (!skill.skill_md_url) {
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: 'text', text: `Skill ${domain}/${skill_id} has no SKILL.md URL.` }],
|
|
138
|
+
isError: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2. Fetch SKILL.md content from publisher's domain
|
|
143
|
+
let content;
|
|
144
|
+
try {
|
|
145
|
+
content = await fetchText(skill.skill_md_url);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: 'text', text: `Failed to fetch SKILL.md from ${skill.skill_md_url}: ${err.message}` }],
|
|
149
|
+
isError: true,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 3. Resolve target tool directory
|
|
154
|
+
let targetTool;
|
|
155
|
+
if (tool) {
|
|
156
|
+
targetTool = tool;
|
|
157
|
+
} else {
|
|
158
|
+
const detected = detectTools();
|
|
159
|
+
if (detected.length === 1) {
|
|
160
|
+
targetTool = detected[0];
|
|
161
|
+
} else if (detected.length > 1) {
|
|
162
|
+
// Default to 'claude' if multiple detected, since we're likely in an MCP context
|
|
163
|
+
targetTool = detected.includes('claude') ? 'claude' : detected[0];
|
|
164
|
+
} else {
|
|
165
|
+
targetTool = 'agents'; // cross-tool fallback
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 4. Write SKILL.md and manifest
|
|
170
|
+
const baseDir = resolveToolDir(targetTool);
|
|
171
|
+
const skillDir = path.join(baseDir, skill_id);
|
|
172
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
173
|
+
|
|
174
|
+
const manifest = {
|
|
175
|
+
source_domain: domain,
|
|
176
|
+
skill_id: skill_id,
|
|
177
|
+
name: skill.name,
|
|
178
|
+
description: skill.description,
|
|
179
|
+
skill_md_url: skill.skill_md_url,
|
|
180
|
+
installed_at: new Date().toISOString(),
|
|
181
|
+
version_hash: hashContent(content),
|
|
182
|
+
tool: targetTool,
|
|
183
|
+
};
|
|
184
|
+
writeSkill(skillDir, content, manifest);
|
|
185
|
+
|
|
186
|
+
// 5. Build response
|
|
187
|
+
const result = {
|
|
188
|
+
success: true,
|
|
189
|
+
path: skillPath,
|
|
190
|
+
verified,
|
|
191
|
+
skill_id,
|
|
192
|
+
domain,
|
|
193
|
+
tool: targetTool,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (!verified) {
|
|
197
|
+
result.warning = 'This skill is unverified. It was submitted by URL, not verified via DNS by the domain owner. Please confirm with the user before they rely on it.';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
202
|
+
};
|
|
203
|
+
} catch (err) {
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: 'text', text: `Error installing skill: ${err.message}` }],
|
|
206
|
+
isError: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// ─── Tool: list_installed ─────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
server.tool(
|
|
215
|
+
'list_installed',
|
|
216
|
+
'List all AgentRoot skills currently installed on this machine. Scans tool directories for skills with AgentRoot manifest files.',
|
|
217
|
+
{},
|
|
218
|
+
async () => {
|
|
219
|
+
try {
|
|
220
|
+
const results = scanInstalled().map(r => ({
|
|
221
|
+
skill_id: r.skill_id,
|
|
222
|
+
domain: r.domain,
|
|
223
|
+
tool: r.tool,
|
|
224
|
+
installed_at: r.installed_at,
|
|
225
|
+
verified: r.manifest.verified !== undefined ? r.manifest.verified : null,
|
|
226
|
+
path: r.path,
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
if (results.length === 0) {
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: 'text', text: 'No AgentRoot skills installed. Use search_skills to find skills and install_skill to install them.' }],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
237
|
+
};
|
|
238
|
+
} catch (err) {
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: 'text', text: `Error listing installed skills: ${err.message}` }],
|
|
241
|
+
isError: true,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// ─── Start Server ──────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
async function main() {
|
|
250
|
+
const transport = new StdioServerTransport();
|
|
251
|
+
await server.connect(transport);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
main().catch((err) => {
|
|
255
|
+
console.error('Fatal error starting AgentRoot MCP server:', err);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-root/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for AgentRoot — search and install AI agent skills from any MCP-compatible client.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"agentroot-mcp-server": "./bin/server.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./bin/server.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"ai",
|
|
17
|
+
"agent",
|
|
18
|
+
"skills",
|
|
19
|
+
"claude",
|
|
20
|
+
"codex",
|
|
21
|
+
"gemini",
|
|
22
|
+
"cursor",
|
|
23
|
+
"agentroot"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/d3-inc/agentroot",
|
|
29
|
+
"directory": "mcp-server"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@agent-root/core": "^0.1.0",
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
}
|
|
38
|
+
}
|