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.
Files changed (2) hide show
  1. package/index.js +97 -9
  2. 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 { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
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.4.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 their price, payment options, and input schema. Call them with call_tool.`,
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 results = searchTools(args.query ?? "", args.limit ?? 10);
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({ results, usage: "call_tool {\"slug\": …, \"params\": …}" }, null, 2)
269
- : `No tools matched "${args.query}". Browse the catalog at ${BASE}/tools or ${BASE}/api/pricing.`,
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent402-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "mcpName": "io.github.MikeyPetrillo/agent402",
5
5
  "author": "Mikey Petrillo (https://github.com/MikeyPetrillo)",
6
6
  "homepage": "https://agent402.tools",