w2a-mcp 0.1.0 → 0.1.1
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 +346 -0
- package/package.json +1 -1
package/index.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* W2A MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes any W2A-enabled site's skills as MCP tools.
|
|
6
|
+
* Works with Claude Desktop, Cursor, Cline, and any MCP client.
|
|
7
|
+
*
|
|
8
|
+
* Install:
|
|
9
|
+
* npm install -g w2a-mcp
|
|
10
|
+
*
|
|
11
|
+
* Add to Claude Desktop config (~/.claude/claude_desktop_config.json):
|
|
12
|
+
* {
|
|
13
|
+
* "mcpServers": {
|
|
14
|
+
* "w2a": {
|
|
15
|
+
* "command": "npx",
|
|
16
|
+
* "args": ["w2a-mcp", "--url", "https://yoursite.com"]
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* Or discover multiple sites:
|
|
22
|
+
* {
|
|
23
|
+
* "mcpServers": {
|
|
24
|
+
* "w2a-stripe": {
|
|
25
|
+
* "command": "npx",
|
|
26
|
+
* "args": ["w2a-mcp", "--url", "stripe.com"]
|
|
27
|
+
* },
|
|
28
|
+
* "w2a-mysite": {
|
|
29
|
+
* "command": "npx",
|
|
30
|
+
* "args": ["w2a-mcp", "--url", "mysite.com"]
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
37
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
38
|
+
import {
|
|
39
|
+
CallToolRequestSchema,
|
|
40
|
+
ListToolsRequestSchema,
|
|
41
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
42
|
+
|
|
43
|
+
const W2A_HEADERS = {
|
|
44
|
+
"Accept": "application/json",
|
|
45
|
+
"Agent-W2A": "1.0",
|
|
46
|
+
"User-Agent": "W2A-MCP-Server/0.1.0 (+https://w2a-protocol.org)",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ── Argument parsing ─────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function parseArgs() {
|
|
52
|
+
const args = process.argv.slice(2);
|
|
53
|
+
const config = { url: null, validate: false, list: false, check: false };
|
|
54
|
+
for (let i = 0; i < args.length; i++) {
|
|
55
|
+
if (args[i] === "--url" && args[i + 1]) {
|
|
56
|
+
config.url = args[++i];
|
|
57
|
+
} else if (args[i] === "--validate") {
|
|
58
|
+
config.validate = true;
|
|
59
|
+
} else if (args[i] === "--list") {
|
|
60
|
+
config.list = true;
|
|
61
|
+
} else if (args[i] === "--check") {
|
|
62
|
+
config.check = true;
|
|
63
|
+
} else if (!args[i].startsWith("--")) {
|
|
64
|
+
config.url = args[i];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!config.url) {
|
|
68
|
+
console.log("W2A MCP Server v0.1.1");
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log("Usage:");
|
|
71
|
+
console.log(" npx w2a-mcp --url <site> start MCP server (for Claude Desktop)");
|
|
72
|
+
console.log(" npx w2a-mcp --url <site> --check check if site is W2A enabled");
|
|
73
|
+
console.log(" npx w2a-mcp --url <site> --list list available skills and exit");
|
|
74
|
+
console.log("");
|
|
75
|
+
console.log("Examples:");
|
|
76
|
+
console.log(" npx w2a-mcp --url stripe.com --check");
|
|
77
|
+
console.log(" npx w2a-mcp --url w2a-protocol.org --list");
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── W2A discovery ─────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function normaliseUrl(url) {
|
|
86
|
+
if (!url.startsWith("http")) url = "https://" + url;
|
|
87
|
+
const parsed = new URL(url);
|
|
88
|
+
return parsed.origin;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function discoverSite(origin) {
|
|
92
|
+
const manifestUrl = `${origin}/.well-known/agents.json`;
|
|
93
|
+
const res = await fetch(manifestUrl, { headers: W2A_HEADERS });
|
|
94
|
+
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`No W2A manifest found at ${manifestUrl} (HTTP ${res.status}). ` +
|
|
98
|
+
`This site hasn't adopted W2A yet. ` +
|
|
99
|
+
`Generate a manifest at https://w2a-protocol.org/tools`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const manifest = await res.json();
|
|
104
|
+
const skills = manifest.skills || manifest.capabilities || [];
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
name: manifest.site?.name || origin,
|
|
108
|
+
type: manifest.site?.type || "other",
|
|
109
|
+
origin,
|
|
110
|
+
skills,
|
|
111
|
+
manifest,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── MCP tool schema builders ─────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
function w2aTypeToJsonSchema(typeStr) {
|
|
118
|
+
const map = {
|
|
119
|
+
"string": { type: "string" },
|
|
120
|
+
"string?": { type: "string" },
|
|
121
|
+
"int": { type: "integer" },
|
|
122
|
+
"int?": { type: "integer" },
|
|
123
|
+
"float": { type: "number" },
|
|
124
|
+
"float?": { type: "number" },
|
|
125
|
+
"bool": { type: "boolean" },
|
|
126
|
+
"bool?": { type: "boolean" },
|
|
127
|
+
"object": { type: "object" },
|
|
128
|
+
"object?": { type: "object" },
|
|
129
|
+
"string[]": { type: "array", items: { type: "string" } },
|
|
130
|
+
"int[]": { type: "array", items: { type: "integer" } },
|
|
131
|
+
"object[]": { type: "array", items: { type: "object" } },
|
|
132
|
+
};
|
|
133
|
+
return map[typeStr] || { type: "string" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildToolSchema(skill) {
|
|
137
|
+
const properties = {};
|
|
138
|
+
const required = [];
|
|
139
|
+
|
|
140
|
+
for (const [name, type] of Object.entries(skill.input || {})) {
|
|
141
|
+
properties[name] = {
|
|
142
|
+
...w2aTypeToJsonSchema(type),
|
|
143
|
+
description: `${name} (${type})`,
|
|
144
|
+
};
|
|
145
|
+
if (!type.endsWith("?")) required.push(name);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties,
|
|
151
|
+
required,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Skill execution ───────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
async function callSkill(site, skill, args) {
|
|
158
|
+
const method = skill.action.split(" ")[0].toUpperCase();
|
|
159
|
+
const path = skill.action.split(" ")[1] || "/";
|
|
160
|
+
|
|
161
|
+
// Replace :param with actual values from args
|
|
162
|
+
const resolvedPath = path.replace(/:([a-zA-Z_]+)/g, (_, name) => {
|
|
163
|
+
if (args[name] !== undefined) {
|
|
164
|
+
const val = args[name];
|
|
165
|
+
const { [name]: _, ...rest } = args;
|
|
166
|
+
args = rest;
|
|
167
|
+
return encodeURIComponent(val);
|
|
168
|
+
}
|
|
169
|
+
return `:${name}`;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
let url = `${site.origin}${resolvedPath}`;
|
|
173
|
+
|
|
174
|
+
const init = {
|
|
175
|
+
method,
|
|
176
|
+
headers: {
|
|
177
|
+
...W2A_HEADERS,
|
|
178
|
+
"Content-Type": "application/json",
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (method === "GET" || method === "DELETE") {
|
|
183
|
+
const params = new URLSearchParams();
|
|
184
|
+
for (const [k, v] of Object.entries(args)) {
|
|
185
|
+
if (v !== undefined && v !== null) params.set(k, String(v));
|
|
186
|
+
}
|
|
187
|
+
const qs = params.toString();
|
|
188
|
+
if (qs) url += `?${qs}`;
|
|
189
|
+
} else {
|
|
190
|
+
init.body = JSON.stringify(args);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const res = await fetch(url, init);
|
|
194
|
+
const text = await res.text();
|
|
195
|
+
|
|
196
|
+
let data;
|
|
197
|
+
try {
|
|
198
|
+
data = JSON.parse(text);
|
|
199
|
+
} catch {
|
|
200
|
+
data = { response: text };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
status: res.status,
|
|
205
|
+
ok: res.ok,
|
|
206
|
+
data,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Server setup ──────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
async function main() {
|
|
213
|
+
const config = parseArgs();
|
|
214
|
+
const origin = normaliseUrl(config.url);
|
|
215
|
+
|
|
216
|
+
let site;
|
|
217
|
+
try {
|
|
218
|
+
site = await discoverSite(origin);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error(`[W2A MCP] Discovery failed: ${err.message}`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// --check: print status and exit
|
|
225
|
+
if (config.check) {
|
|
226
|
+
console.log(`W2A enabled: yes`);
|
|
227
|
+
console.log(`Site name: ${site.name}`);
|
|
228
|
+
console.log(`Site type: ${site.type}`);
|
|
229
|
+
console.log(`Skills: ${site.skills.length}`);
|
|
230
|
+
console.log(`A2A compat: ${site.manifest.a2a_profile ? "yes" : "no"}`);
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --list: print skills table and exit
|
|
235
|
+
if (config.list) {
|
|
236
|
+
console.log(`\n${site.name} — ${site.skills.length} skill${site.skills.length !== 1 ? "s" : ""}\n`);
|
|
237
|
+
site.skills.forEach(s => {
|
|
238
|
+
console.log(` ${s.id.padEnd(32)} ${s.action.padEnd(30)} [auth: ${s.auth}]`);
|
|
239
|
+
console.log(` ${"".padEnd(32)} ${s.intent}`);
|
|
240
|
+
console.log();
|
|
241
|
+
});
|
|
242
|
+
process.exit(0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const server = new Server(
|
|
246
|
+
{
|
|
247
|
+
name: `w2a-${site.name.toLowerCase().replace(/[^a-z0-9]/g, "-")}`,
|
|
248
|
+
version: "0.1.0",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
capabilities: { tools: {} },
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// List tools — one per W2A skill
|
|
256
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
257
|
+
const tools = site.skills.map((skill) => ({
|
|
258
|
+
name: skill.id,
|
|
259
|
+
description: `${skill.intent} (${skill.action}) [auth: ${skill.auth}]`,
|
|
260
|
+
inputSchema: buildToolSchema(skill),
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
// Always add a discovery tool so the agent can see the full manifest
|
|
264
|
+
tools.push({
|
|
265
|
+
name: "w2a_site_info",
|
|
266
|
+
description: `Get full W2A manifest and capability list for ${site.name}`,
|
|
267
|
+
inputSchema: { type: "object", properties: {} },
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return { tools };
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Call tool
|
|
274
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
275
|
+
const { name, arguments: args } = request.params;
|
|
276
|
+
|
|
277
|
+
if (name === "w2a_site_info") {
|
|
278
|
+
return {
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: "text",
|
|
282
|
+
text: JSON.stringify(
|
|
283
|
+
{
|
|
284
|
+
site: site.name,
|
|
285
|
+
type: site.type,
|
|
286
|
+
origin: site.origin,
|
|
287
|
+
skills: site.skills.map((s) => ({
|
|
288
|
+
id: s.id,
|
|
289
|
+
intent: s.intent,
|
|
290
|
+
action: s.action,
|
|
291
|
+
auth: s.auth,
|
|
292
|
+
})),
|
|
293
|
+
a2a_compatible: !!site.manifest.a2a_profile,
|
|
294
|
+
},
|
|
295
|
+
null,
|
|
296
|
+
2
|
|
297
|
+
),
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const skill = site.skills.find((s) => s.id === name);
|
|
304
|
+
if (!skill) {
|
|
305
|
+
return {
|
|
306
|
+
content: [
|
|
307
|
+
{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: `Skill '${name}' not found. Available: ${site.skills.map((s) => s.id).join(", ")}`,
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
isError: true,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const result = await callSkill(site, skill, args || {});
|
|
318
|
+
return {
|
|
319
|
+
content: [
|
|
320
|
+
{
|
|
321
|
+
type: "text",
|
|
322
|
+
text: JSON.stringify(result.data, null, 2),
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
isError: !result.ok,
|
|
326
|
+
};
|
|
327
|
+
} catch (err) {
|
|
328
|
+
return {
|
|
329
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
330
|
+
isError: true,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const transport = new StdioServerTransport();
|
|
336
|
+
await server.connect(transport);
|
|
337
|
+
|
|
338
|
+
console.error(
|
|
339
|
+
`[W2A MCP] Connected to ${site.name} — ${site.skills.length} skills available`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
main().catch((err) => {
|
|
344
|
+
console.error("[W2A MCP] Fatal error:", err);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
});
|