agent402-mcp 0.5.0 → 0.7.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 +97 -9
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -19,11 +19,16 @@
|
|
|
19
19
|
import { createHash } from "node:crypto";
|
|
20
20
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
CallToolRequestSchema,
|
|
24
|
+
ListToolsRequestSchema,
|
|
25
|
+
ListPromptsRequestSchema,
|
|
26
|
+
GetPromptRequestSchema,
|
|
27
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
23
28
|
|
|
24
29
|
const BASE = (process.env.AGENT402_URL || "https://agent402.tools").replace(/\/$/, "");
|
|
25
30
|
const AGENT_KEY = process.env.AGENT_KEY || "";
|
|
26
|
-
const VERSION = "0.
|
|
31
|
+
const VERSION = "0.6.0";
|
|
27
32
|
|
|
28
33
|
// Spend controls — enforced BEFORE a payment is ever signed, so a confused or
|
|
29
34
|
// runaway model cannot drain the wallet. Unset = unlimited (back-compat).
|
|
@@ -46,12 +51,19 @@ const log = (...a) => console.error("[agent402-mcp]", ...a);
|
|
|
46
51
|
// ---------------------------------------------------------------------------
|
|
47
52
|
// Catalog: built from the service's own machine-readable surfaces.
|
|
48
53
|
const catalog = new Map(); // slug -> { slug, method, path, price, description, category, computePayable, inputSchema }
|
|
54
|
+
// Skill packs — curated multi-tool workflows, fetched at startup from the
|
|
55
|
+
// hosted service so the npm package picks up new packs without a republish.
|
|
56
|
+
// Empty if the discovery fetch fails (older services or transient errors);
|
|
57
|
+
// prompts/list will just return an empty array in that case.
|
|
58
|
+
let skillPacks = [];
|
|
49
59
|
|
|
50
60
|
async function loadCatalog() {
|
|
51
|
-
const [pricing, openapi] = await Promise.all([
|
|
61
|
+
const [pricing, openapi, packs] = await Promise.all([
|
|
52
62
|
fetch(`${BASE}/api/pricing`).then((r) => r.json()),
|
|
53
63
|
fetch(`${BASE}/openapi.json`).then((r) => r.json()),
|
|
64
|
+
fetch(`${BASE}/api/skill-packs.json`).then((r) => (r.ok ? r.json() : { packs: [] })).catch(() => ({ packs: [] })),
|
|
54
65
|
]);
|
|
66
|
+
skillPacks = Array.isArray(packs?.packs) ? packs.packs : [];
|
|
55
67
|
for (const e of pricing.endpoints) {
|
|
56
68
|
const slug = e.slug ?? e.docs?.split("/tools/").pop();
|
|
57
69
|
if (!slug) continue;
|
|
@@ -207,9 +219,78 @@ function searchTools(query, limit = 10) {
|
|
|
207
219
|
}));
|
|
208
220
|
}
|
|
209
221
|
|
|
222
|
+
// Rank the curated multi-tool skill packs against the same query, so a single
|
|
223
|
+
// search_tools call also tells the agent "this looks like a `security-audit`
|
|
224
|
+
// or `email-deliverability` job — fetch the whole template via prompts/get".
|
|
225
|
+
// Weighted slug/title/tagline/useCase/toolSlugs match — same shape as the
|
|
226
|
+
// hosted /api/find ranking, kept inline here so the stdio package stays
|
|
227
|
+
// dependency-free. Returns [] when no pack scores above the noise floor.
|
|
228
|
+
function rankWorkflows(query, k = 2) {
|
|
229
|
+
const terms = String(query).toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1);
|
|
230
|
+
if (!terms.length || !skillPacks.length) return [];
|
|
231
|
+
const scored = [];
|
|
232
|
+
for (const p of skillPacks) {
|
|
233
|
+
const slug = p.slug.toLowerCase();
|
|
234
|
+
const title = (p.title || "").toLowerCase();
|
|
235
|
+
const tagline = (p.tagline || "").toLowerCase();
|
|
236
|
+
const useCase = (p.useCase || "").toLowerCase();
|
|
237
|
+
const toolSet = new Set((p.toolSlugs || []).map((s) => String(s).toLowerCase()));
|
|
238
|
+
const workflowHay = (p.workflow || []).join(" ").toLowerCase();
|
|
239
|
+
let score = 0;
|
|
240
|
+
for (const term of terms) {
|
|
241
|
+
if (slug === term) score += 12;
|
|
242
|
+
else if (slug.includes(term)) score += 5;
|
|
243
|
+
if (title.includes(term)) score += 3;
|
|
244
|
+
if (tagline.includes(term)) score += 2;
|
|
245
|
+
if (useCase.includes(term)) score += 1;
|
|
246
|
+
if (toolSet.has(term)) score += 4;
|
|
247
|
+
if (workflowHay.includes(term)) score += 1;
|
|
248
|
+
}
|
|
249
|
+
if (score >= 4) scored.push([score, p]);
|
|
250
|
+
}
|
|
251
|
+
scored.sort((a, b) => b[0] - a[0] || a[1].slug.length - b[1].slug.length);
|
|
252
|
+
return scored.slice(0, k).map(([score, p]) => ({
|
|
253
|
+
slug: p.slug,
|
|
254
|
+
title: p.title,
|
|
255
|
+
tagline: p.tagline,
|
|
256
|
+
toolCount: (p.toolSlugs || []).length,
|
|
257
|
+
promptName: p.slug,
|
|
258
|
+
score,
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
|
|
210
262
|
// ---------------------------------------------------------------------------
|
|
211
263
|
// MCP wiring
|
|
212
|
-
const server = new Server({ name: "agent402", version: VERSION }, { capabilities: { tools: {} } });
|
|
264
|
+
const server = new Server({ name: "agent402", version: VERSION }, { capabilities: { tools: {}, prompts: {} } });
|
|
265
|
+
|
|
266
|
+
// Skill packs are exposed as MCP prompts — discoverable in slash menus on any
|
|
267
|
+
// MCP-aware client. The list is fetched once at boot in loadCatalog(); the
|
|
268
|
+
// per-prompt rendering is delegated to the hosted service so the npm package
|
|
269
|
+
// stays thin and prompt text stays canonical with the website at /skills.
|
|
270
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
271
|
+
prompts: skillPacks.map((p) => ({
|
|
272
|
+
name: p.slug,
|
|
273
|
+
title: p.title,
|
|
274
|
+
description: p.tagline,
|
|
275
|
+
arguments: (p.promptArgs || []).map((a) => ({
|
|
276
|
+
name: a.name,
|
|
277
|
+
description: a.description,
|
|
278
|
+
required: a.required ?? true,
|
|
279
|
+
})),
|
|
280
|
+
})),
|
|
281
|
+
}));
|
|
282
|
+
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
283
|
+
const { name, arguments: args = {} } = req.params;
|
|
284
|
+
const pack = skillPacks.find((p) => p.slug === name);
|
|
285
|
+
if (!pack) throw new Error(`Unknown prompt "${name}". List available with prompts/list.`);
|
|
286
|
+
const url = new URL(`${BASE}/api/skill-packs/${encodeURIComponent(name)}/prompt`);
|
|
287
|
+
for (const [k, v] of Object.entries(args || {})) {
|
|
288
|
+
if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v));
|
|
289
|
+
}
|
|
290
|
+
const res = await fetch(url);
|
|
291
|
+
if (!res.ok) throw new Error(`Failed to render prompt "${name}" from ${BASE}: HTTP ${res.status}`);
|
|
292
|
+
return await res.json();
|
|
293
|
+
});
|
|
213
294
|
|
|
214
295
|
let curated = [];
|
|
215
296
|
let pricingInfo = null;
|
|
@@ -224,7 +305,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
224
305
|
{
|
|
225
306
|
name: "search_tools",
|
|
226
307
|
description:
|
|
227
|
-
`Search the full Agent402 catalog (${catalog.size} pay-per-call tools: encoding, crypto, data conversion, text, time, validation, math, unit conversions, network, browser, memory). Returns matching tools with
|
|
308
|
+
`Search the full Agent402 catalog (${catalog.size} pay-per-call tools: encoding, crypto, data conversion, text, time, validation, math, unit conversions, network, browser, memory). Returns matching tools with price, payment options, and input schema — call them with call_tool. Also returns matching multi-tool workflow templates (skill packs) when the query is task-shaped; fetch the whole template via prompts/get { name: "<slug>", arguments: { … } }.`,
|
|
228
309
|
inputSchema: {
|
|
229
310
|
type: "object",
|
|
230
311
|
properties: {
|
|
@@ -260,13 +341,19 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
260
341
|
const { name, arguments: args = {} } = req.params;
|
|
261
342
|
try {
|
|
262
343
|
if (name === "search_tools") {
|
|
263
|
-
const
|
|
344
|
+
const q = args.query ?? "";
|
|
345
|
+
const results = searchTools(q, args.limit ?? 10);
|
|
346
|
+
const workflows = rankWorkflows(q, 2);
|
|
264
347
|
return {
|
|
265
348
|
content: [{
|
|
266
349
|
type: "text",
|
|
267
|
-
text: results.length
|
|
268
|
-
? JSON.stringify({
|
|
269
|
-
|
|
350
|
+
text: results.length || workflows.length
|
|
351
|
+
? JSON.stringify({
|
|
352
|
+
results,
|
|
353
|
+
...(workflows.length ? { workflows, workflowsUsage: "prompts/get { name: workflows[i].promptName, arguments: { …per-pack args } } returns the full Claude-ready task template." } : {}),
|
|
354
|
+
usage: "call_tool {\"slug\": …, \"params\": …}",
|
|
355
|
+
}, null, 2)
|
|
356
|
+
: `No tools matched "${q}". Browse the catalog at ${BASE}/tools or ${BASE}/api/pricing.`,
|
|
270
357
|
}],
|
|
271
358
|
};
|
|
272
359
|
}
|
|
@@ -288,6 +375,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
288
375
|
tools: catalog.size,
|
|
289
376
|
payableWithCompute: computePayable,
|
|
290
377
|
walletOnly: catalog.size - computePayable,
|
|
378
|
+
workflows: skillPacks.length,
|
|
291
379
|
spendControls: AGENT_KEY
|
|
292
380
|
? {
|
|
293
381
|
maxPerCallUsd: MAX_PER_CALL === Infinity ? "unlimited" : MAX_PER_CALL,
|