w2a-mcp 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/package.json +35 -0
- package/src/index.js +308 -0
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "w2a-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "W2A MCP Server — expose any W2A-enabled site as MCP tools for Claude, Cursor, and other MCP clients",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"w2a-mcp": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"w2a",
|
|
16
|
+
"web2agent",
|
|
17
|
+
"ai-agents",
|
|
18
|
+
"claude",
|
|
19
|
+
"cursor",
|
|
20
|
+
"model-context-protocol"
|
|
21
|
+
],
|
|
22
|
+
"author": "W2A Protocol",
|
|
23
|
+
"license": "Apache-2.0",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/Nijjwol23/w2a"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://w2a-protocol.org",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
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 };
|
|
54
|
+
for (let i = 0; i < args.length; i++) {
|
|
55
|
+
if (args[i] === "--url" && args[i + 1]) config.url = args[++i];
|
|
56
|
+
if (args[i] === "--validate") config.validate = true;
|
|
57
|
+
}
|
|
58
|
+
if (!config.url) {
|
|
59
|
+
console.error("Usage: w2a-mcp --url <site-url>");
|
|
60
|
+
console.error("Example: w2a-mcp --url stripe.com");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── W2A discovery ─────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function normaliseUrl(url) {
|
|
69
|
+
if (!url.startsWith("http")) url = "https://" + url;
|
|
70
|
+
const parsed = new URL(url);
|
|
71
|
+
return parsed.origin;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function discoverSite(origin) {
|
|
75
|
+
const manifestUrl = `${origin}/.well-known/agents.json`;
|
|
76
|
+
const res = await fetch(manifestUrl, { headers: W2A_HEADERS });
|
|
77
|
+
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`No W2A manifest found at ${manifestUrl} (HTTP ${res.status}). ` +
|
|
81
|
+
`This site hasn't adopted W2A yet. ` +
|
|
82
|
+
`Generate a manifest at https://w2a-protocol.org/tools`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const manifest = await res.json();
|
|
87
|
+
const skills = manifest.skills || manifest.capabilities || [];
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
name: manifest.site?.name || origin,
|
|
91
|
+
type: manifest.site?.type || "other",
|
|
92
|
+
origin,
|
|
93
|
+
skills,
|
|
94
|
+
manifest,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── MCP tool schema builders ─────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function w2aTypeToJsonSchema(typeStr) {
|
|
101
|
+
const map = {
|
|
102
|
+
"string": { type: "string" },
|
|
103
|
+
"string?": { type: "string" },
|
|
104
|
+
"int": { type: "integer" },
|
|
105
|
+
"int?": { type: "integer" },
|
|
106
|
+
"float": { type: "number" },
|
|
107
|
+
"float?": { type: "number" },
|
|
108
|
+
"bool": { type: "boolean" },
|
|
109
|
+
"bool?": { type: "boolean" },
|
|
110
|
+
"object": { type: "object" },
|
|
111
|
+
"object?": { type: "object" },
|
|
112
|
+
"string[]": { type: "array", items: { type: "string" } },
|
|
113
|
+
"int[]": { type: "array", items: { type: "integer" } },
|
|
114
|
+
"object[]": { type: "array", items: { type: "object" } },
|
|
115
|
+
};
|
|
116
|
+
return map[typeStr] || { type: "string" };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildToolSchema(skill) {
|
|
120
|
+
const properties = {};
|
|
121
|
+
const required = [];
|
|
122
|
+
|
|
123
|
+
for (const [name, type] of Object.entries(skill.input || {})) {
|
|
124
|
+
properties[name] = {
|
|
125
|
+
...w2aTypeToJsonSchema(type),
|
|
126
|
+
description: `${name} (${type})`,
|
|
127
|
+
};
|
|
128
|
+
if (!type.endsWith("?")) required.push(name);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties,
|
|
134
|
+
required,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Skill execution ───────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
async function callSkill(site, skill, args) {
|
|
141
|
+
const method = skill.action.split(" ")[0].toUpperCase();
|
|
142
|
+
const path = skill.action.split(" ")[1] || "/";
|
|
143
|
+
|
|
144
|
+
// Replace :param with actual values from args
|
|
145
|
+
const resolvedPath = path.replace(/:([a-zA-Z_]+)/g, (_, name) => {
|
|
146
|
+
if (args[name] !== undefined) {
|
|
147
|
+
const val = args[name];
|
|
148
|
+
const { [name]: _, ...rest } = args;
|
|
149
|
+
args = rest;
|
|
150
|
+
return encodeURIComponent(val);
|
|
151
|
+
}
|
|
152
|
+
return `:${name}`;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
let url = `${site.origin}${resolvedPath}`;
|
|
156
|
+
|
|
157
|
+
const init = {
|
|
158
|
+
method,
|
|
159
|
+
headers: {
|
|
160
|
+
...W2A_HEADERS,
|
|
161
|
+
"Content-Type": "application/json",
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (method === "GET" || method === "DELETE") {
|
|
166
|
+
const params = new URLSearchParams();
|
|
167
|
+
for (const [k, v] of Object.entries(args)) {
|
|
168
|
+
if (v !== undefined && v !== null) params.set(k, String(v));
|
|
169
|
+
}
|
|
170
|
+
const qs = params.toString();
|
|
171
|
+
if (qs) url += `?${qs}`;
|
|
172
|
+
} else {
|
|
173
|
+
init.body = JSON.stringify(args);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const res = await fetch(url, init);
|
|
177
|
+
const text = await res.text();
|
|
178
|
+
|
|
179
|
+
let data;
|
|
180
|
+
try {
|
|
181
|
+
data = JSON.parse(text);
|
|
182
|
+
} catch {
|
|
183
|
+
data = { response: text };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
status: res.status,
|
|
188
|
+
ok: res.ok,
|
|
189
|
+
data,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Server setup ──────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
async function main() {
|
|
196
|
+
const config = parseArgs();
|
|
197
|
+
const origin = normaliseUrl(config.url);
|
|
198
|
+
|
|
199
|
+
let site;
|
|
200
|
+
try {
|
|
201
|
+
site = await discoverSite(origin);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error(`[W2A MCP] Discovery failed: ${err.message}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const server = new Server(
|
|
208
|
+
{
|
|
209
|
+
name: `w2a-${site.name.toLowerCase().replace(/[^a-z0-9]/g, "-")}`,
|
|
210
|
+
version: "0.1.0",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
capabilities: { tools: {} },
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// List tools — one per W2A skill
|
|
218
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
219
|
+
const tools = site.skills.map((skill) => ({
|
|
220
|
+
name: skill.id,
|
|
221
|
+
description: `${skill.intent} (${skill.action}) [auth: ${skill.auth}]`,
|
|
222
|
+
inputSchema: buildToolSchema(skill),
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
// Always add a discovery tool so the agent can see the full manifest
|
|
226
|
+
tools.push({
|
|
227
|
+
name: "w2a_site_info",
|
|
228
|
+
description: `Get full W2A manifest and capability list for ${site.name}`,
|
|
229
|
+
inputSchema: { type: "object", properties: {} },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return { tools };
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Call tool
|
|
236
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
237
|
+
const { name, arguments: args } = request.params;
|
|
238
|
+
|
|
239
|
+
if (name === "w2a_site_info") {
|
|
240
|
+
return {
|
|
241
|
+
content: [
|
|
242
|
+
{
|
|
243
|
+
type: "text",
|
|
244
|
+
text: JSON.stringify(
|
|
245
|
+
{
|
|
246
|
+
site: site.name,
|
|
247
|
+
type: site.type,
|
|
248
|
+
origin: site.origin,
|
|
249
|
+
skills: site.skills.map((s) => ({
|
|
250
|
+
id: s.id,
|
|
251
|
+
intent: s.intent,
|
|
252
|
+
action: s.action,
|
|
253
|
+
auth: s.auth,
|
|
254
|
+
})),
|
|
255
|
+
a2a_compatible: !!site.manifest.a2a_profile,
|
|
256
|
+
},
|
|
257
|
+
null,
|
|
258
|
+
2
|
|
259
|
+
),
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const skill = site.skills.find((s) => s.id === name);
|
|
266
|
+
if (!skill) {
|
|
267
|
+
return {
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: "text",
|
|
271
|
+
text: `Skill '${name}' not found. Available: ${site.skills.map((s) => s.id).join(", ")}`,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
isError: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const result = await callSkill(site, skill, args || {});
|
|
280
|
+
return {
|
|
281
|
+
content: [
|
|
282
|
+
{
|
|
283
|
+
type: "text",
|
|
284
|
+
text: JSON.stringify(result.data, null, 2),
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
isError: !result.ok,
|
|
288
|
+
};
|
|
289
|
+
} catch (err) {
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
292
|
+
isError: true,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const transport = new StdioServerTransport();
|
|
298
|
+
await server.connect(transport);
|
|
299
|
+
|
|
300
|
+
console.error(
|
|
301
|
+
`[W2A MCP] Connected to ${site.name} — ${site.skills.length} skills available`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
main().catch((err) => {
|
|
306
|
+
console.error("[W2A MCP] Fatal error:", err);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
});
|