@apiosk/mcp 1.3.1 → 1.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/src/runtime.mjs CHANGED
@@ -17,10 +17,130 @@ import {
17
17
  saveLocalApioskConfig,
18
18
  } from "./local-config.mjs";
19
19
  import { createLocalWalletStore } from "./wallet-store.mjs";
20
+ import { logToolCall } from "./observability.mjs";
21
+ import {
22
+ hostedCreateWalletToken,
23
+ hostedCreateWalletUnavailable,
24
+ hostedDeleteWallet,
25
+ hostedDeleteWalletToken,
26
+ hostedListWallets,
27
+ hostedListWalletTokens,
28
+ hostedUpdateWallet,
29
+ hostedUpdateWalletToken,
30
+ hostedWalletActivity,
31
+ } from "./hosted-wallets.mjs";
32
+ import {
33
+ PUBLISHER_TOOLS,
34
+ handlePublisherTool,
35
+ isPublisherTool,
36
+ } from "./publisher.mjs";
37
+ import { DISCOVER_TOOL, runDiscover } from "./discovery.mjs";
38
+ import { searchKnownSources } from "./source-registry.mjs";
39
+ import { INSPECT_TOOL, runInspect } from "./x402-inspect.mjs";
40
+ import { FETCH_PAID_TOOL, runFetchPaid } from "./external-fetch.mjs";
20
41
 
21
42
  const DEFAULT_LIMIT = 25;
22
43
  const CACHE_TTL_MS = 60_000;
23
44
  const DEFAULT_GATEWAY_BASE_URL = "https://gateway.apiosk.com";
45
+ const HOSTED_OAUTH_SCOPE = "mcp:tools";
46
+
47
+ // Permissive default output schema advertised for every tool that does not
48
+ // declare its own. MCP clients (ChatGPT, Claude, …) flag tools with no
49
+ // outputSchema as "output schema recommended"; declaring one clears that hint.
50
+ // Apiosk tool results are always returned as a JSON object in structuredContent
51
+ // (see content()), so an open object schema both documents that contract and
52
+ // validates every success payload without risking false rejections on the
53
+ // varied per-tool result shapes. Individual tools can still ship a tighter
54
+ // schema (e.g. dynamic listings via listing_metadata.mcp_tool.outputSchema),
55
+ // which is preserved as-is.
56
+ const DEFAULT_TOOL_OUTPUT_SCHEMA = {
57
+ type: "object",
58
+ additionalProperties: true,
59
+ description:
60
+ "Structured JSON result of the tool call. Mirrors the human-readable text content; the exact fields depend on the tool (an `error` field is present when the call fails).",
61
+ };
62
+
63
+ // Behaviour hints (MCP tool annotations). Clients like ChatGPT default an
64
+ // un-annotated tool to the most cautious badges — "destructive" and
65
+ // "open world" — so every write tool that ships no annotations reads as
66
+ // dangerous. These presets make the badges accurate: wallet/account/keystore
67
+ // management is a closed domain (the user's own Apiosk account), publishing
68
+ // wires up an external upstream (open world), and deletes are destructive.
69
+ const READ_ONLY_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
70
+ const CREATE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
71
+ const UPDATE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
72
+ const DELETE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false };
73
+ const SETUP_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
74
+ const PUBLISH_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
75
+ const PUBLISH_UPDATE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true };
76
+ const PUBLISH_DELETE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true };
77
+ // Dynamic per-listing tools call arbitrary paid third-party APIs and spend
78
+ // USDC, so an unspecified listing gets the same hints as apiosk_execute.
79
+ const DYNAMIC_EXECUTE_ANNOTATIONS = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true };
80
+
81
+ // Static tools whose literals don't already declare `annotations`. Discovery
82
+ // and publisher (sk_live_) tools annotate themselves inline and are absent here.
83
+ const TOOL_ANNOTATIONS = {
84
+ // Managed (dashboard) wallet + account tools — closed Apiosk-account domain.
85
+ apiosk_list_wallets: READ_ONLY_ANNOTATIONS,
86
+ apiosk_get_wallet_activity: READ_ONLY_ANNOTATIONS,
87
+ apiosk_list_wallet_api_keys: READ_ONLY_ANNOTATIONS,
88
+ apiosk_show_wallet_funding: READ_ONLY_ANNOTATIONS,
89
+ apiosk_create_wallet: CREATE_ANNOTATIONS,
90
+ apiosk_create_wallet_connect_string: CREATE_ANNOTATIONS,
91
+ apiosk_create_wallet_api_key: CREATE_ANNOTATIONS,
92
+ apiosk_create_account: CREATE_ANNOTATIONS,
93
+ apiosk_update_wallet: UPDATE_ANNOTATIONS,
94
+ apiosk_update_wallet_api_key: UPDATE_ANNOTATIONS,
95
+ apiosk_sign_in: UPDATE_ANNOTATIONS,
96
+ apiosk_delete_wallet: DELETE_ANNOTATIONS,
97
+ apiosk_delete_wallet_api_key: DELETE_ANNOTATIONS,
98
+ // Local (stdio) wallet keystore tools.
99
+ apiosk_wallet_list: READ_ONLY_ANNOTATIONS,
100
+ apiosk_wallet_reveal_secret: READ_ONLY_ANNOTATIONS,
101
+ apiosk_configure: READ_ONLY_ANNOTATIONS,
102
+ apiosk_wallet_create: CREATE_ANNOTATIONS,
103
+ apiosk_wallet_select: UPDATE_ANNOTATIONS,
104
+ apiosk_wallet_update: UPDATE_ANNOTATIONS,
105
+ apiosk_wallet_save_secret: UPDATE_ANNOTATIONS,
106
+ apiosk_wallet_delete: DELETE_ANNOTATIONS,
107
+ apiosk_get_started: SETUP_ANNOTATIONS,
108
+ // Wallet-signed publishing — registers an external upstream (open world).
109
+ apiosk_list_my_apis: READ_ONLY_ANNOTATIONS,
110
+ apiosk_publish_api: PUBLISH_ANNOTATIONS,
111
+ apiosk_update_api: PUBLISH_UPDATE_ANNOTATIONS,
112
+ apiosk_delete_api: PUBLISH_DELETE_ANNOTATIONS,
113
+ };
114
+
115
+ // Applied at the single list-emission boundary (runtime.listTools): fills in a
116
+ // default output schema and behaviour annotations for any tool that doesn't
117
+ // already declare its own, so clients stop flagging "output schema recommended"
118
+ // and render accurate read-only/destructive/open-world badges.
119
+ function normalizeToolForClient(tool, { hosted = false, protectedTool = false } = {}) {
120
+ if (!tool || typeof tool !== "object") return tool;
121
+ let next = tool;
122
+ if (!next.outputSchema) {
123
+ next = { ...next, outputSchema: DEFAULT_TOOL_OUTPUT_SCHEMA };
124
+ }
125
+ if (!next.annotations && TOOL_ANNOTATIONS[next.name]) {
126
+ next = { ...next, annotations: TOOL_ANNOTATIONS[next.name] };
127
+ }
128
+ if (hosted && !next.securitySchemes) {
129
+ const securitySchemes = protectedTool
130
+ ? [{ type: "oauth2", scopes: [HOSTED_OAUTH_SCOPE] }]
131
+ : [{ type: "noauth" }];
132
+ next = {
133
+ ...next,
134
+ securitySchemes,
135
+ _meta: {
136
+ ...(next._meta || {}),
137
+ // Back-compat mirror required by older ChatGPT connector clients.
138
+ securitySchemes,
139
+ },
140
+ };
141
+ }
142
+ return next;
143
+ }
24
144
 
25
145
  const DASHBOARD_WALLET_TOOLS = [
26
146
  {
@@ -502,7 +622,7 @@ const PUBLISH_TOOLS = [
502
622
 
503
623
  const HELP_TOOL = {
504
624
  name: "apiosk_help",
505
- description: "Explain what Apiosk MCP is, how to connect it, how auth and x402 payments work, the settlement rails (USDC and credits), and the recommended workflow for discovery, wallets, and publishing.",
625
+ description: "Explain what Apiosk MCP is, how to connect it, how auth and USDC/x402 payments work, and the recommended workflow for discovery, wallets, and publishing.",
506
626
  annotations: {
507
627
  readOnlyHint: true,
508
628
  openWorldHint: false,
@@ -513,8 +633,8 @@ const HELP_TOOL = {
513
633
  properties: {
514
634
  topic: {
515
635
  type: "string",
516
- enum: ["overview", "setup", "auth", "workflow", "payments", "rails", "wallets", "publish", "configure"],
517
- description: "Optional help topic. Defaults to overview. Use 'rails' to learn how settlement works across USDC and credits.",
636
+ enum: ["overview", "setup", "auth", "workflow", "discovery", "payments", "rails", "wallets", "publish", "configure"],
637
+ description: "Optional help topic. Defaults to overview. Use 'discovery' to learn which live sources apiosk_discover searches (Apiosk catalog + Coinbase Bazaar + well-known); use 'rails' for how USDC/x402 settlement works.",
518
638
  },
519
639
  },
520
640
  },
@@ -559,7 +679,7 @@ const EXPLORE_TOOL = {
559
679
 
560
680
  const SEARCH_TOOL = {
561
681
  name: "apiosk_search",
562
- description: "Search and browse the Apiosk catalog. Use this first when you need to find APIs by capability, price, or category.",
682
+ description: "Search and browse the Apiosk catalog by capability, price, or category. For browsing/filtering the catalog. When the goal is to fulfil a user request with real paid data ('get me the live X'), prefer apiosk_discover, which decomposes the need and ranks the best endpoints across sources.",
563
683
  annotations: {
564
684
  readOnlyHint: true,
565
685
  openWorldHint: false,
@@ -639,6 +759,12 @@ const EXECUTE_TOOL = {
639
759
  openWorldHint: true,
640
760
  destructiveHint: true,
641
761
  },
762
+ _meta: {
763
+ "openai/outputTemplate": "ui://apiosk/result-canvas.html",
764
+ "openai/toolInvocation/invoking": "Fetching and paying for data…",
765
+ "openai/toolInvocation/invoked": "Paid data received",
766
+ ui: { resourceUri: "ui://apiosk/result-canvas.html" },
767
+ },
642
768
  inputSchema: {
643
769
  type: "object",
644
770
  required: ["slug"],
@@ -688,7 +814,7 @@ const HEALTH_TOOL = {
688
814
  const PAYMENT_GUIDE_TOOL = {
689
815
  name: "apiosk_payment_guide",
690
816
  description:
691
- "Explain how to pay through the Apiosk gateway. Returns a buyer guide (how an agent settles a paid API call over USDC/x402 or credits, tailored to the current auth) and a provider guide (how to publish an API and get paid). Pass slug to scope buyer guidance to one listing, or role to pick a side.",
817
+ "Explain how to pay through the Apiosk gateway. Returns a buyer guide (how an agent settles a paid API call over USDC/x402, tailored to the current auth) and a provider guide (how to publish an API and get paid). Pass slug to scope buyer guidance to one listing, or role to pick a side.",
692
818
  annotations: {
693
819
  readOnlyHint: true,
694
820
  openWorldHint: false,
@@ -715,15 +841,21 @@ const DISCOVERY_TOOLS = [
715
841
  PAYMENT_GUIDE_TOOL,
716
842
  EXPLORE_TOOL,
717
843
  SEARCH_TOOL,
844
+ DISCOVER_TOOL,
845
+ INSPECT_TOOL,
718
846
  GET_API_TOOL,
719
847
  EXECUTE_TOOL,
720
848
  ];
721
849
  // Discovery + payment guidance available to every remote buyer, even before
722
- // they authorize (these tools are public / read-only).
850
+ // they authorize (these tools are public / read-only). apiosk_discover and
851
+ // apiosk_inspect_x402 are read-only too — they find candidate x402 endpoints and
852
+ // read their 402 terms without spending, so buyers can plan a purchase pre-auth.
723
853
  const HOSTED_DISCOVERY_TOOLS = [
724
854
  HELP_TOOL,
725
855
  PAYMENT_GUIDE_TOOL,
726
856
  SEARCH_TOOL,
857
+ DISCOVER_TOOL,
858
+ INSPECT_TOOL,
727
859
  EXPLORE_TOOL,
728
860
  GET_API_TOOL,
729
861
  METADATA_TOOL,
@@ -734,18 +866,61 @@ const HOSTED_DISCOVERY_TOOLS = [
734
866
  // Managed buyer tools that work over request-scoped dashboard auth on the
735
867
  // hosted endpoint: prepaid credits + full managed agent-wallet CRUD. These are
736
868
  // protected (the OAuth layer requires authorization before they run).
737
- // apiosk_show_wallet_funding is intentionally excluded — it resolves a local /
869
+ // apiosk_show_wallet_funding is intentionally excluded, it resolves a local /
738
870
  // env wallet, which does not exist on the hosted surface.
739
871
  const HOSTED_MANAGED_TOOLS = [
740
- ...REMOTE_CREDITS_TOOLS,
741
872
  ...DASHBOARD_WALLET_TOOLS.filter((tool) => tool.name !== "apiosk_show_wallet_funding"),
742
873
  ];
743
874
 
744
875
  // The hosted/remote surface is fully capable: discovery, payment guidance,
745
876
  // generic + dynamic per-API execution, credits, and managed-wallet management.
746
- // Publishing stays local/portal-only because it requires a client-side signing
747
- // key the hosted server never holds.
748
- const HOSTED_REMOTE_TOOLS = [...HOSTED_DISCOVERY_TOOLS, ...HOSTED_MANAGED_TOOLS];
877
+ // Wallet-signed publishing (apiosk_publish_api & co.) stays local/portal-only
878
+ // because it requires a client-side signing key the hosted server never
879
+ // holds; the x402 publisher tools work hosted because they authenticate with
880
+ // a provider token (sk_live_…) instead of a wallet.
881
+ // External-payment buyer tool: pay an x402 endpoint the gateway does NOT host,
882
+ // from the connected managed wallet, via the gateway payer proxy. Protected
883
+ // (spends USDC) and available in every mode that can present a connect token.
884
+ const EXTERNAL_PAY_TOOLS = [FETCH_PAID_TOOL];
885
+
886
+ const HOSTED_REMOTE_TOOLS = [
887
+ ...HOSTED_DISCOVERY_TOOLS,
888
+ ...HOSTED_MANAGED_TOOLS,
889
+ ...EXTERNAL_PAY_TOOLS,
890
+ ...PUBLISHER_TOOLS,
891
+ ];
892
+
893
+ // Lean BUYER surface for the hosted connector. A buyer opening the connector in
894
+ // ChatGPT/Claude should see the agentic flow + one wallet view — not 28 tools.
895
+ // Ordered flow-first. The advanced wallet CRUD (create/update/delete wallet +
896
+ // api-keys), explore/metadata/health, and provider publishing are dropped from
897
+ // the default list. Nothing is lost: every hidden tool is still dispatchable by
898
+ // name (callTool + isToolProtected are unchanged) and the full set is one env
899
+ // flag away (APIOSK_MCP_FULL_TOOLS=true). apiosk_list_wallets stays so buyers can
900
+ // see their wallet address, spend limits, and funding.
901
+ const HOSTED_BUYER_TOOLS = [
902
+ HELP_TOOL,
903
+ DISCOVER_TOOL,
904
+ EXPLORE_TOOL,
905
+ INSPECT_TOOL,
906
+ EXECUTE_TOOL,
907
+ FETCH_PAID_TOOL,
908
+ GET_API_TOOL,
909
+ SEARCH_TOOL,
910
+ PAYMENT_GUIDE_TOOL,
911
+ DASHBOARD_WALLET_TOOLS.find((tool) => tool.name === "apiosk_list_wallets"),
912
+ ].filter(Boolean);
913
+
914
+ // PROVIDER surface (caller authenticated with a sk_live_ provider key): a focused
915
+ // publishing toolkit. The managed-wallet CRUD needs a dashboard JWT a provider
916
+ // key does not carry, so those tools would fail anyway — they're excluded here.
917
+ const HOSTED_PROVIDER_TOOLS = [
918
+ HELP_TOOL,
919
+ PAYMENT_GUIDE_TOOL,
920
+ SEARCH_TOOL,
921
+ GET_API_TOOL,
922
+ ...PUBLISHER_TOOLS,
923
+ ];
749
924
 
750
925
  const ALL_STATIC_TOOLS = [
751
926
  ...DISCOVERY_TOOLS,
@@ -754,15 +929,28 @@ const ALL_STATIC_TOOLS = [
754
929
  ...LOCAL_WALLET_TOOLS,
755
930
  ...DASHBOARD_WALLET_TOOLS,
756
931
  ...PUBLISH_TOOLS,
932
+ ...PUBLISHER_TOOLS,
757
933
  ];
758
934
 
935
+ // These tools read or mutate client-machine state (local files, local signing
936
+ // keys, or a local dashboard session). They must never execute in the hosted
937
+ // multi-tenant server, even if a stale MCP client cached an older tool list or
938
+ // the deployment accidentally carries APIOSK_ENABLE_LOCAL_WALLETS=true.
939
+ const LOCAL_ONLY_TOOL_NAMES = new Set([
940
+ ...LOCAL_ACCOUNT_AND_CREDITS_TOOLS.map((tool) => tool.name),
941
+ ...LOCAL_WALLET_TOOLS.map((tool) => tool.name),
942
+ ...PUBLISH_TOOLS.map((tool) => tool.name),
943
+ ]);
944
+
759
945
  const PUBLIC_STATIC_TOOL_NAMES = new Set(
760
946
  DISCOVERY_TOOLS.map((tool) => tool.name).filter((name) => name !== "apiosk_execute")
761
947
  );
762
948
  const REMOTE_PROTECTED_STATIC_TOOL_NAMES = new Set([
763
949
  "apiosk_execute",
950
+ ...EXTERNAL_PAY_TOOLS.map((tool) => tool.name),
764
951
  ...REMOTE_CREDITS_TOOLS.map((tool) => tool.name),
765
952
  ...DASHBOARD_WALLET_TOOLS.map((tool) => tool.name),
953
+ ...PUBLISHER_TOOLS.map((tool) => tool.name),
766
954
  ]);
767
955
 
768
956
  function trimString(value) {
@@ -786,7 +974,7 @@ function sanitizeToolName(name, fallback) {
786
974
  * active but unverified -> pending review, active + verified -> live.
787
975
  *
788
976
  * NOTE: the public catalog (GET /v1/apis) hard-filters to active AND verified
789
- * rows, so entries built from it are always "live" — the field documents that
977
+ * rows, so entries built from it are always "live", the field documents that
790
978
  * invariant for catalog readers. The pending/disabled branches are reached when
791
979
  * this runs over a richer object (e.g. a get_api_detail response, which returns
792
980
  * the real active/verified flags).
@@ -821,6 +1009,18 @@ function buildCatalogEntry(api, toolName) {
821
1009
  };
822
1010
  }
823
1011
 
1012
+ function normalizeCatalogToken(value) {
1013
+ return String(value || "")
1014
+ .trim()
1015
+ .toLowerCase()
1016
+ .replace(/[^a-z0-9]+/g, "-")
1017
+ .replace(/^-+|-+$/g, "");
1018
+ }
1019
+
1020
+ function isWeatherLikeSlug(slug) {
1021
+ return /weather|meteo|forecast/.test(normalizeCatalogToken(slug));
1022
+ }
1023
+
824
1024
  function buildDynamicTools(catalog, reservedTools) {
825
1025
  const tools = [];
826
1026
  const toolIndex = new Map();
@@ -872,7 +1072,16 @@ function buildDynamicTools(catalog, reservedTools) {
872
1072
  type: "object",
873
1073
  additionalProperties: true,
874
1074
  },
875
- annotations: api.listing_metadata?.mcp_tool?.annotations,
1075
+ outputSchema:
1076
+ api.listing_metadata?.mcp_tool?.outputSchema || DEFAULT_TOOL_OUTPUT_SCHEMA,
1077
+ annotations:
1078
+ api.listing_metadata?.mcp_tool?.annotations || DYNAMIC_EXECUTE_ANNOTATIONS,
1079
+ _meta: {
1080
+ "openai/outputTemplate": "ui://apiosk/result-canvas.html",
1081
+ "openai/toolInvocation/invoking": `Fetching ${api.name || api.slug}…`,
1082
+ "openai/toolInvocation/invoked": `${api.name || api.slug} received`,
1083
+ ui: { resourceUri: "ui://apiosk/result-canvas.html" },
1084
+ },
876
1085
  });
877
1086
 
878
1087
  toolIndex.set(toolName, {
@@ -1202,6 +1411,35 @@ function buildHelpPayload(topic = "overview", options = {}) {
1202
1411
  : "Use APIOSK_PRIVATE_KEY if you need autonomous payment on the public server mode",
1203
1412
  ],
1204
1413
  },
1414
+ discovery: {
1415
+ topic: "discovery",
1416
+ summary:
1417
+ "apiosk_discover searches the whole x402 ecosystem in ONE call and returns ranked endpoints. This is the full list of sources it can explore. Call it whenever the user wants real/live/paid data.",
1418
+ searched_by_default: [
1419
+ "apiosk — the Apiosk catalog: first-party listings PLUS federated externals (imported from providers' /.well-known/x402, the APILayer & ApyHub ecosystems, direct provider integrations, and selected MCP skills). Paid via apiosk_execute.",
1420
+ "bazaar — the Coinbase x402 Bazaar (~25k resources), searched LIVE every call. The central shared index most marketplaces + MCPs publish into. Paid via apiosk_fetch_paid.",
1421
+ ],
1422
+ opt_in_pass_sources_or_all: [
1423
+ "x402-list — x402-list.com public directory (free REST).",
1424
+ "x402-direct — x402.direct search engine with trust scores (free REST).",
1425
+ "agentic-market — Coinbase Agentic.Market directory (free REST).",
1426
+ "thirdweb — public thirdweb Payments x402 resource index (free REST).",
1427
+ "payai — public PayAI facilitator discovery mirror (free REST).",
1428
+ "x402engine — direct manifest with paid AI/media/code/web endpoints.",
1429
+ "anchor-x402 — direct manifest with paid primitives and LLM endpoints.",
1430
+ "x402scan — paid full-text resource search; returned as an inspect/fetch-paid pointer, never auto-paid.",
1431
+ "apify — paid x402 prepaid-token endpoint plus the public Actor catalog; never auto-paid.",
1432
+ "wellknown — probe a specific host's /.well-known/x402 (needs probe_hosts).",
1433
+ "Use sources:['all'] to fan out to all directly wired free REST sources at once. Paid sources remain explicit opt-ins.",
1434
+ ],
1435
+ indexed_or_reference_only: [
1436
+ "x402list.fun (paid MCP), awesome-x402 (markdown), and facilitators Nevermined / @swader / x402.rs — reference, not directly searchable.",
1437
+ ],
1438
+ important_note:
1439
+ "These are DISCOVERY sources (where the agent LOOKS for endpoints) — NOT the same as a catalog listing's `source` field (where one existing entry was imported from). The Bazaar is queried by default, so external endpoints surface without passing sources; pass sources:['all'] to also sweep the other public directories.",
1440
+ how_to:
1441
+ "apiosk_discover({ query, segments, sources }) → one ranked list; each result tagged with `source`, `trust_tier`, and `executable_via`.",
1442
+ },
1205
1443
  payments: {
1206
1444
  topic: "payments",
1207
1445
  summary: "Paid Apiosk APIs can return x402 payment requirements if the client is not configured to settle automatically.",
@@ -1238,10 +1476,10 @@ function buildHelpPayload(topic = "overview", options = {}) {
1238
1476
  "The connect string identifies the buyer's managed wallet and connect token; APIO_WALLET_* limits bound the USDC rail. See help topic 'setup' for the connect string format.",
1239
1477
  provider_settlement: {
1240
1478
  what_it_is:
1241
- "The settlement_rails above are how BUYERS pay. This is the PROVIDER (seller) side: how an API owner receives their earnings. Agents do not interact with it — it is provider-account configuration in the provider portal.",
1479
+ "The settlement_rails above are how BUYERS pay. This is the PROVIDER (seller) side: how an API owner receives their earnings. Agents do not interact with it, it is provider-account configuration in the provider portal.",
1242
1480
  how_it_works: [
1243
1481
  "USDC earnings settle to the API's payout wallet (apis.wallet_address), which the portal links to a verified, Monerium-linked payout_wallets row.",
1244
- "Crypto -> EUR off-ramp mandate (optional): the provider signs an EIP-191 authorization and an on-chain mandate (ApioskOfframpExecutor on Base). Apiosk's offramp keeper then auto-converts their accumulated USDC to EURe via Monerium and redeems it to their IBAN over SEPA once the balance crosses their bundle threshold. Non-custodial — the keeper can never redirect funds or exceed the provider's on-chain per-run cap or cooldown.",
1482
+ "Crypto -> EUR off-ramp mandate (optional): the provider signs an EIP-191 authorization and an on-chain mandate (ApioskOfframpExecutor on Base). Apiosk's offramp keeper then auto-converts their accumulated USDC to EURe via Monerium and redeems it to their IBAN over SEPA once the balance crosses their bundle threshold. Non-custodial, the keeper can never redirect funds or exceed the provider's on-chain per-run cap or cooldown.",
1245
1483
  ],
1246
1484
  },
1247
1485
  },
@@ -1278,7 +1516,7 @@ function buildHelpPayload(topic = "overview", options = {}) {
1278
1516
  "slug must use lowercase letters, numbers, and hyphens",
1279
1517
  ],
1280
1518
  identity_note:
1281
- "These tools manage listings via WALLET SIGNATURE — the community/MCP publish channel, owned by a synthetic platform account. That is a different identity from the provider portal, where providers manage APIs under a Supabase-auth account (owner_id) after Monerium KYB, and where listings auto-verify on create. A listing published here and one created in the portal are managed through different identities and do not currently round-trip between the two surfaces.",
1519
+ "These tools manage listings via WALLET SIGNATURE, the community/MCP publish channel, owned by a synthetic platform account. That is a different identity from the provider portal, where providers manage APIs under a Supabase-auth account (owner_id) after Monerium KYB, and where listings auto-verify on create. A listing published here and one created in the portal are managed through different identities and do not currently round-trip between the two surfaces.",
1282
1520
  },
1283
1521
  configure: {
1284
1522
  topic: "configure",
@@ -1367,8 +1605,10 @@ export function createApioskMcpRuntime(options = {}) {
1367
1605
  const providedDashboardManager = options.walletManager || null;
1368
1606
  const hostedAuthEnabled = options.hostedAuthEnabled === true;
1369
1607
  const localWalletStore =
1370
- options.localWalletStore ||
1371
- (options.enableLocalWallets === false ? null : createLocalWalletStore(env));
1608
+ hostedAuthEnabled
1609
+ ? null
1610
+ : options.localWalletStore ||
1611
+ (options.enableLocalWallets === false ? null : createLocalWalletStore(env));
1372
1612
  const cache = {
1373
1613
  catalog: null,
1374
1614
  expiresAt: 0,
@@ -1378,6 +1618,7 @@ export function createApioskMcpRuntime(options = {}) {
1378
1618
  };
1379
1619
 
1380
1620
  async function getActiveExecutionWallet() {
1621
+ if (hostedAuthEnabled) return null;
1381
1622
  try {
1382
1623
  const envWallet = resolveEnvPrivateWallet(env);
1383
1624
  if (envWallet) return envWallet;
@@ -1390,6 +1631,7 @@ export function createApioskMcpRuntime(options = {}) {
1390
1631
  }
1391
1632
 
1392
1633
  async function getSavedConfig() {
1634
+ if (hostedAuthEnabled) return null;
1393
1635
  return readLocalApioskConfig(env);
1394
1636
  }
1395
1637
 
@@ -1551,7 +1793,16 @@ export function createApioskMcpRuntime(options = {}) {
1551
1793
 
1552
1794
  function getStaticTools(authInfo = null) {
1553
1795
  if (hostedAuthEnabled) {
1554
- return [...HOSTED_REMOTE_TOOLS];
1796
+ // Escape hatch: expose the full 28-tool surface when explicitly opted in.
1797
+ if (env.APIOSK_MCP_FULL_TOOLS === "true") {
1798
+ return [...HOSTED_REMOTE_TOOLS];
1799
+ }
1800
+ // Providers authenticate with a sk_live_ key → focused publishing surface.
1801
+ if (trimString(authInfo?.extra?.apiosk_provider_key)) {
1802
+ return [...HOSTED_PROVIDER_TOOLS];
1803
+ }
1804
+ // Buyers (OAuth / connect token / pre-auth) → the lean agentic surface.
1805
+ return [...HOSTED_BUYER_TOOLS];
1555
1806
  }
1556
1807
 
1557
1808
  const tools = [...DISCOVERY_TOOLS];
@@ -1562,6 +1813,14 @@ export function createApioskMcpRuntime(options = {}) {
1562
1813
  tools.push(...DASHBOARD_WALLET_TOOLS);
1563
1814
  }
1564
1815
 
1816
+ // Provider-token x402 publishing works in every mode: hosted callers pass
1817
+ // Authorization: Bearer sk_live_…, stdio callers set APIOSK_PROVIDER_TOKEN.
1818
+ tools.push(...PUBLISHER_TOOLS);
1819
+
1820
+ // External x402 pay works with any connect token (env APIOSK_CONNECT_TOKEN in
1821
+ // stdio, or a request-scoped token on the hosted surface).
1822
+ tools.push(...EXTERNAL_PAY_TOOLS);
1823
+
1565
1824
  return tools;
1566
1825
  }
1567
1826
 
@@ -1580,6 +1839,51 @@ export function createApioskMcpRuntime(options = {}) {
1580
1839
  return cache.catalog;
1581
1840
  }
1582
1841
 
1842
+ async function resolveExecutableApiSlug(slug, authInfo = null) {
1843
+ const requested = normalizeCatalogToken(slug);
1844
+ if (!requested) return slug;
1845
+
1846
+ const catalog = await getCatalog(false, authInfo);
1847
+ const exact = catalog.find((api) => normalizeCatalogToken(api.slug) === requested);
1848
+ if (exact) return exact.slug;
1849
+
1850
+ const aliases = new Map([
1851
+ ["weather", "open-meteo"],
1852
+ ["weather-api", "open-meteo"],
1853
+ ["hugen-weather", "open-meteo"],
1854
+ ["open-meteo-weather", "open-meteo"],
1855
+ ]);
1856
+ const alias = aliases.get(requested);
1857
+ if (alias && catalog.some((api) => normalizeCatalogToken(api.slug) === alias)) {
1858
+ return alias;
1859
+ }
1860
+
1861
+ if (isWeatherLikeSlug(requested)) {
1862
+ const weather = catalog.find((api) => {
1863
+ const haystack = [
1864
+ api.slug,
1865
+ api.name,
1866
+ api.description,
1867
+ api.category,
1868
+ api.raw_category,
1869
+ ...(Array.isArray(api.listing_metadata?.tags) ? api.listing_metadata.tags : []),
1870
+ ]
1871
+ .map(normalizeCatalogToken)
1872
+ .join(" ");
1873
+
1874
+ return (
1875
+ api.active !== false &&
1876
+ api.verified !== false &&
1877
+ /weather|meteo|forecast/.test(haystack)
1878
+ );
1879
+ });
1880
+
1881
+ if (weather?.slug) return weather.slug;
1882
+ }
1883
+
1884
+ return slug;
1885
+ }
1886
+
1583
1887
  async function getTools(force = false, authInfo = null) {
1584
1888
  if (!force && cache.dynamicTools && Date.now() < cache.expiresAt) {
1585
1889
  return [...getStaticTools(authInfo), ...cache.dynamicTools];
@@ -1993,12 +2297,20 @@ export function createApioskMcpRuntime(options = {}) {
1993
2297
  });
1994
2298
 
1995
2299
  const catalog = response.apis || [];
2300
+ const sourceMatches = searchKnownSources(argumentsObject.search, {
2301
+ limit: argumentsObject.limit || DEFAULT_LIMIT,
2302
+ });
1996
2303
  await getTools(false, authInfo);
1997
2304
  const capability = await resolvePaymentCapability(authInfo);
1998
2305
 
1999
2306
  return content({
2000
2307
  apis: catalog.map((api) => buildCatalogEntry(api, cache.toolNamesBySlug.get(api.slug) || null)),
2308
+ sources: sourceMatches,
2001
2309
  meta: response.meta,
2310
+ source_meta: {
2311
+ total: sourceMatches.length,
2312
+ note: "These are direct x402 discovery sources, not Apiosk catalog API listings. Their public and paid endpoints are included inline.",
2313
+ },
2002
2314
  payment: buildDiscoveryPaymentHint({
2003
2315
  capability,
2004
2316
  mode: capability.mode,
@@ -2007,7 +2319,9 @@ export function createApioskMcpRuntime(options = {}) {
2007
2319
  for_providers:
2008
2320
  "Listing your own API? Call apiosk_payment_guide with role='provider' (or apiosk_publish_api) to publish it for other agents.",
2009
2321
  next_steps:
2010
- "Call apiosk_get_api for full metadata plus a per-listing payment block, use the API-specific tool directly when tool_name is present, or call apiosk_payment_guide to learn exactly how to settle a paid call.",
2322
+ sourceMatches.length > 0
2323
+ ? "For a matched source, pass its discover_source to apiosk_discover. For endpoints marked payment_required=true, call apiosk_inspect_x402 first and only then apiosk_fetch_paid after the user confirms the live price. Catalog APIs still use apiosk_get_api/apiosk_execute."
2324
+ : "Call apiosk_get_api for full metadata plus a per-listing payment block, use the API-specific tool directly when tool_name is present, or call apiosk_payment_guide to learn exactly how to settle a paid call.",
2011
2325
  });
2012
2326
  }
2013
2327
 
@@ -2079,13 +2393,53 @@ export function createApioskMcpRuntime(options = {}) {
2079
2393
  });
2080
2394
  }
2081
2395
 
2396
+ // Agentic discovery: aggregate + rank candidate x402 endpoints across sources
2397
+ // (Phase 1: the Apiosk catalog, which already includes federated externals).
2398
+ // Reuses the request-scoped client so catalog reads honour the same gateway
2399
+ // base URL and connect-token threading as every other tool.
2400
+ async function handleDiscover(argumentsObject = {}, authInfo = null) {
2401
+ const client = await getClient(authInfo);
2402
+ const savedConfig = await getSavedConfig().catch(() => null);
2403
+ return runDiscover(argumentsObject, {
2404
+ listApis: (params) => client.listApis(params),
2405
+ gatewayBaseUrl: resolveGatewayBaseUrl(env, savedConfig),
2406
+ });
2407
+ }
2408
+
2409
+ // Read an arbitrary URL's x402 402 terms without paying. Read-only probe; no
2410
+ // wallet/connect token is used, so it needs no auth.
2411
+ async function handleInspect(argumentsObject = {}, authInfo = null) {
2412
+ const savedConfig = await getSavedConfig().catch(() => null);
2413
+ let gatewayHost = "";
2414
+ try {
2415
+ gatewayHost = new URL(resolveGatewayBaseUrl(env, savedConfig)).hostname;
2416
+ } catch {
2417
+ gatewayHost = "";
2418
+ }
2419
+ return runInspect(argumentsObject, { gatewayHost });
2420
+ }
2421
+
2422
+ // Pay an external (non-Apiosk-hosted) x402 endpoint through the gateway payer
2423
+ // proxy, using the same connect token the runtime threads to the gateway for
2424
+ // catalog settlement. The gateway enforces spend caps and does the signing.
2425
+ async function handleFetchPaid(argumentsObject = {}, authInfo = null) {
2426
+ const savedConfig = await getSavedConfig().catch(() => null);
2427
+ const requestConnectToken = trimString(authInfo?.extra?.apiosk_connect_token);
2428
+ const envConnectToken = trimString(env.APIOSK_CONNECT_TOKEN || savedConfig?.connect_token);
2429
+ return runFetchPaid(argumentsObject, {
2430
+ connectToken: requestConnectToken || envConnectToken,
2431
+ gatewayBaseUrl: resolveGatewayBaseUrl(env, savedConfig),
2432
+ });
2433
+ }
2434
+
2082
2435
  async function handleExecute(argumentsObject = {}, authInfo = null) {
2083
2436
  if (!argumentsObject.slug) {
2084
2437
  return errorContent("Missing required field: slug");
2085
2438
  }
2086
2439
 
2087
2440
  const client = await getClient(authInfo);
2088
- const result = await client.execute(argumentsObject.slug, argumentsObject.input, {
2441
+ const resolvedSlug = await resolveExecutableApiSlug(argumentsObject.slug, authInfo);
2442
+ const result = await client.execute(resolvedSlug, argumentsObject.input, {
2089
2443
  operation: argumentsObject.operation,
2090
2444
  query: argumentsObject.query,
2091
2445
  pathParams: argumentsObject.path_params,
@@ -2094,7 +2448,15 @@ export function createApioskMcpRuntime(options = {}) {
2094
2448
  },
2095
2449
  });
2096
2450
 
2097
- return content(result);
2451
+ return content(
2452
+ resolvedSlug === argumentsObject.slug
2453
+ ? result
2454
+ : {
2455
+ requested_slug: argumentsObject.slug,
2456
+ resolved_slug: resolvedSlug,
2457
+ result,
2458
+ }
2459
+ );
2098
2460
  }
2099
2461
 
2100
2462
  async function handleDynamicExecute(tool, argumentsObject = {}, authInfo = null) {
@@ -2286,11 +2648,37 @@ export function createApioskMcpRuntime(options = {}) {
2286
2648
  });
2287
2649
  }
2288
2650
 
2651
+ // Hosted (OAuth / wallet sign-in) sessions manage wallets straight against
2652
+ // Supabase REST with the caller's own JWT — the legacy dashboard backend
2653
+ // these tools used to proxy to no longer exists (dashboard.apiosk.com is now
2654
+ // the provider-portal SPA, whose catch-all answers /api/* with index.html).
2655
+ // The requestDashboard proxy path is kept only for stdio setups that point
2656
+ // APIOSK_CONTROL_PLANE_URL at their own control plane.
2657
+ function hostedWalletContext(authInfo = null) {
2658
+ const sessionToken = resolveRequestDashboardUserToken(authInfo);
2659
+ if (!sessionToken) return null;
2660
+ return {
2661
+ env,
2662
+ sessionToken,
2663
+ userId: trimString(authInfo?.extra?.userId),
2664
+ };
2665
+ }
2666
+
2289
2667
  async function handleWalletList(authInfo = null) {
2668
+ const hosted = hostedWalletContext(authInfo);
2669
+ if (hosted) {
2670
+ return content(await hostedListWallets(hosted));
2671
+ }
2290
2672
  return content(await requestDashboard("/api/agent-wallets", {}, {}, authInfo));
2291
2673
  }
2292
2674
 
2293
2675
  async function handleWalletCreate(argumentsObject = {}, authInfo = null) {
2676
+ const hosted = hostedWalletContext(authInfo);
2677
+ if (hosted) {
2678
+ // Key derivation + encryption lived in the retired dashboard backend, so
2679
+ // hosted sessions get an honest explanation instead of proxied HTML.
2680
+ return errorContent(hostedCreateWalletUnavailable());
2681
+ }
2294
2682
  return content(
2295
2683
  await requestDashboard(
2296
2684
  "/api/agent-wallets",
@@ -2309,6 +2697,22 @@ export function createApioskMcpRuntime(options = {}) {
2309
2697
  return errorContent("Missing required field: wallet_id");
2310
2698
  }
2311
2699
 
2700
+ const hosted = hostedWalletContext(authInfo);
2701
+ if (hosted) {
2702
+ return content(
2703
+ await hostedUpdateWallet({
2704
+ ...hosted,
2705
+ walletId: argumentsObject.wallet_id,
2706
+ label: argumentsObject.label,
2707
+ status: argumentsObject.status,
2708
+ dailyLimitUsdc: argumentsObject.daily_limit_usdc,
2709
+ perTxLimitUsdc: argumentsObject.per_tx_limit_usdc,
2710
+ color: argumentsObject.color,
2711
+ icon: argumentsObject.icon,
2712
+ })
2713
+ );
2714
+ }
2715
+
2312
2716
  const { wallet_id, ...updates } = argumentsObject;
2313
2717
  return content(
2314
2718
  await requestDashboard(
@@ -2328,6 +2732,13 @@ export function createApioskMcpRuntime(options = {}) {
2328
2732
  return errorContent("Missing required field: wallet_id");
2329
2733
  }
2330
2734
 
2735
+ const hosted = hostedWalletContext(authInfo);
2736
+ if (hosted) {
2737
+ return content(
2738
+ await hostedDeleteWallet({ ...hosted, walletId: argumentsObject.wallet_id })
2739
+ );
2740
+ }
2741
+
2331
2742
  return content(
2332
2743
  await requestDashboard(
2333
2744
  `/api/agent-wallets/${argumentsObject.wallet_id}`,
@@ -2345,6 +2756,17 @@ export function createApioskMcpRuntime(options = {}) {
2345
2756
  return errorContent("Missing required field: wallet_id");
2346
2757
  }
2347
2758
 
2759
+ const hosted = hostedWalletContext(authInfo);
2760
+ if (hosted) {
2761
+ return content(
2762
+ await hostedWalletActivity({
2763
+ ...hosted,
2764
+ walletId: argumentsObject.wallet_id,
2765
+ limit: argumentsObject.limit,
2766
+ })
2767
+ );
2768
+ }
2769
+
2348
2770
  const params = new URLSearchParams();
2349
2771
  if (argumentsObject.page !== undefined) params.set("page", String(argumentsObject.page));
2350
2772
  if (argumentsObject.limit !== undefined) params.set("limit", String(argumentsObject.limit));
@@ -2365,6 +2787,20 @@ export function createApioskMcpRuntime(options = {}) {
2365
2787
  return errorContent("Missing required field: wallet_id");
2366
2788
  }
2367
2789
 
2790
+ const hosted = hostedWalletContext(authInfo);
2791
+ if (hosted) {
2792
+ const created = await hostedCreateWalletToken({
2793
+ ...hosted,
2794
+ walletId: argumentsObject.wallet_id,
2795
+ name: argumentsObject.token_name,
2796
+ revokeExisting: argumentsObject.revoke_existing === true,
2797
+ });
2798
+ return content({
2799
+ ...created,
2800
+ connect_string: `apiosk:connect:${created.connect_token}`,
2801
+ });
2802
+ }
2803
+
2368
2804
  const { wallet_id, ...body } = argumentsObject;
2369
2805
  return content(
2370
2806
  await requestDashboard(
@@ -2384,6 +2820,13 @@ export function createApioskMcpRuntime(options = {}) {
2384
2820
  return errorContent("Missing required field: wallet_id");
2385
2821
  }
2386
2822
 
2823
+ const hosted = hostedWalletContext(authInfo);
2824
+ if (hosted) {
2825
+ return content(
2826
+ await hostedListWalletTokens({ ...hosted, walletId: argumentsObject.wallet_id })
2827
+ );
2828
+ }
2829
+
2387
2830
  return content(
2388
2831
  await requestDashboard(
2389
2832
  `/api/agent-wallets/${argumentsObject.wallet_id}/api-keys`,
@@ -2399,6 +2842,19 @@ export function createApioskMcpRuntime(options = {}) {
2399
2842
  return errorContent("Missing required field: wallet_id");
2400
2843
  }
2401
2844
 
2845
+ const hosted = hostedWalletContext(authInfo);
2846
+ if (hosted) {
2847
+ return content(
2848
+ await hostedCreateWalletToken({
2849
+ ...hosted,
2850
+ walletId: argumentsObject.wallet_id,
2851
+ name: argumentsObject.name,
2852
+ expirationDays: argumentsObject.expiration_days,
2853
+ revokeExisting: argumentsObject.revoke_existing === true,
2854
+ })
2855
+ );
2856
+ }
2857
+
2402
2858
  const { wallet_id, ...body } = argumentsObject;
2403
2859
  return content(
2404
2860
  await requestDashboard(
@@ -2421,6 +2877,20 @@ export function createApioskMcpRuntime(options = {}) {
2421
2877
  return errorContent("Missing required field: key_id");
2422
2878
  }
2423
2879
 
2880
+ const hosted = hostedWalletContext(authInfo);
2881
+ if (hosted) {
2882
+ return content(
2883
+ await hostedUpdateWalletToken({
2884
+ ...hosted,
2885
+ walletId: argumentsObject.wallet_id,
2886
+ keyId: argumentsObject.key_id,
2887
+ name: argumentsObject.name,
2888
+ expirationDays: argumentsObject.expiration_days,
2889
+ revoke: argumentsObject.revoke,
2890
+ })
2891
+ );
2892
+ }
2893
+
2424
2894
  const { wallet_id, key_id, ...body } = argumentsObject;
2425
2895
  return content(
2426
2896
  await requestDashboard(
@@ -2443,6 +2913,17 @@ export function createApioskMcpRuntime(options = {}) {
2443
2913
  return errorContent("Missing required field: key_id");
2444
2914
  }
2445
2915
 
2916
+ const hosted = hostedWalletContext(authInfo);
2917
+ if (hosted) {
2918
+ return content(
2919
+ await hostedDeleteWalletToken({
2920
+ ...hosted,
2921
+ walletId: argumentsObject.wallet_id,
2922
+ keyId: argumentsObject.key_id,
2923
+ })
2924
+ );
2925
+ }
2926
+
2446
2927
  return content(
2447
2928
  await requestDashboard(
2448
2929
  `/api/agent-wallets/${argumentsObject.wallet_id}/api-keys/${argumentsObject.key_id}`,
@@ -2482,7 +2963,7 @@ export function createApioskMcpRuntime(options = {}) {
2482
2963
  }
2483
2964
 
2484
2965
  // Force include_qr_data_url so we can append an inline image content
2485
- // block below — clients that render images (Claude Desktop, MCP
2966
+ // block below, clients that render images (Claude Desktop, MCP
2486
2967
  // Inspector) will then show the funding QR right next to the
2487
2968
  // newly-created wallet without a follow-up tool call.
2488
2969
  created.configure = await buildConfigurePayload({
@@ -2535,7 +3016,7 @@ export function createApioskMcpRuntime(options = {}) {
2535
3016
  `Token: USDC (${baseUsdcContract})`,
2536
3017
  "",
2537
3018
  "WARNING: only Base mainnet USDC. Sending from Ethereum, Polygon, Solana,",
2538
- "or any other network will permanently lose the funds — this address is",
3019
+ "or any other network will permanently lose the funds, this address is",
2539
3020
  "Base only.",
2540
3021
  "",
2541
3022
  `Block explorer: ${receive.explorer_url}`,
@@ -2550,7 +3031,7 @@ export function createApioskMcpRuntime(options = {}) {
2550
3031
 
2551
3032
  // Render the QR inline via MCP image content when the buyer's client
2552
3033
  // supports images (Claude Desktop, the MCP Inspector, etc.). Falls back
2553
- // gracefully on terminals without image support — the text block above
3034
+ // gracefully on terminals without image support, the text block above
2554
3035
  // still has the address and the ANSI QR.
2555
3036
  const dataUrl = receive.qr_code_data_url;
2556
3037
  if (typeof dataUrl === "string" && dataUrl.startsWith("data:image/png;base64,")) {
@@ -3030,12 +3511,92 @@ export function createApioskMcpRuntime(options = {}) {
3030
3511
  });
3031
3512
  }
3032
3513
 
3514
+ // A 402 means the buyer's configured rails could not cover the call. Tell
3515
+ // the caller what THEIR next step is, based on how this request
3516
+ // authenticated, instead of always giving local-stdio advice.
3517
+ function buildPaymentRequiredHint(authInfo = null) {
3518
+ const extra = authInfo?.extra || {};
3519
+ const connectWallet = trimString(
3520
+ extra.apiosk_connect_wallet_address || extra.apioskConnectWalletAddress
3521
+ );
3522
+
3523
+ if (trimString(extra.apiosk_connect_token)) {
3524
+ const walletNote = connectWallet ? ` (${connectWallet})` : "";
3525
+ return (
3526
+ `Your managed Apiosk wallet${walletNote} could not cover this call — it is out of USDC, ` +
3527
+ "over its spending limit, or missing settlement approval. Send USDC on Base mainnet " +
3528
+ "(chain 8453) to the wallet address and retry; payment then settles automatically. " +
3529
+ "Use apiosk_list_wallets to see your wallets and funding instructions."
3530
+ );
3531
+ }
3532
+
3533
+ if (hasRequestScopedDashboardAccess(authInfo)) {
3534
+ return (
3535
+ "You are signed in, but no payable managed wallet is linked to this session. " +
3536
+ "Use apiosk_list_wallets to check your wallets: if one exists, fund it with USDC on Base mainnet " +
3537
+ "and re-authorize the Apiosk app so a fresh payment token is minted; if none exists, set one up " +
3538
+ "in the Apiosk buyer portal first."
3539
+ );
3540
+ }
3541
+
3542
+ return hostedAuthEnabled
3543
+ ? "Authorize Apiosk in your MCP client, then use apiosk_list_wallets to confirm that a funded managed wallet is linked before retrying."
3544
+ : "Run apiosk_get_started in the local stdio package, or configure APIOSK_PRIVATE_KEY, to enable automatic x402 settlement.";
3545
+ }
3546
+
3547
+ // Observability wrapper: time + log every tools/call dispatch to mcp_tool_calls
3548
+ // (fire-and-forget — a logging failure never affects the tool result). See
3549
+ // observability.mjs. Raw tokens/args are never persisted (hash + key-names only).
3033
3550
  async function callTool(name, argumentsObject = {}, authInfo = null) {
3551
+ const startedAt = Date.now();
3552
+ let outcome = "ok";
3553
+ let errorCode = null;
3554
+ try {
3555
+ const result = await dispatchTool(name, argumentsObject, authInfo);
3556
+ if (result && result.isError) outcome = "error";
3557
+ else if (result && result.status === "payment_required") outcome = "refused";
3558
+ return result;
3559
+ } catch (error) {
3560
+ outcome = "error";
3561
+ errorCode = (error && (error.code || error.name)) || null;
3562
+ throw error;
3563
+ } finally {
3564
+ try {
3565
+ logToolCall(env, {
3566
+ toolName: name,
3567
+ outcome,
3568
+ errorCode,
3569
+ latencyMs: Date.now() - startedAt,
3570
+ authInfo,
3571
+ argKeys: argumentsObject && typeof argumentsObject === "object" ? Object.keys(argumentsObject) : [],
3572
+ });
3573
+ } catch {
3574
+ /* observability must never break a tool call */
3575
+ }
3576
+ }
3577
+ }
3578
+
3579
+ async function dispatchTool(name, argumentsObject = {}, authInfo = null) {
3034
3580
  try {
3581
+ if (hostedAuthEnabled && LOCAL_ONLY_TOOL_NAMES.has(name)) {
3582
+ return content({
3583
+ status: "unsupported",
3584
+ error_code: "tool.local_only",
3585
+ message: `${name} is only available in the local stdio package and cannot run on the hosted Apiosk MCP server.`,
3586
+ next_steps: [
3587
+ "Use apiosk_search, apiosk_explore, or apiosk_discover anonymously.",
3588
+ "For paid or account-specific actions, authorize Apiosk in the MCP client and use apiosk_list_wallets or apiosk_execute.",
3589
+ ],
3590
+ });
3591
+ }
3592
+
3035
3593
  if (name === "apiosk_help") return await handleHelp(argumentsObject);
3036
3594
  if (name === "apiosk_payment_guide") return await handlePaymentGuide(argumentsObject, authInfo);
3037
3595
  if (name === "apiosk_explore") return await handleExplore(argumentsObject, authInfo);
3038
3596
  if (name === "apiosk_search") return await handleSearch(argumentsObject, authInfo);
3597
+ if (name === "apiosk_discover") return await handleDiscover(argumentsObject, authInfo);
3598
+ if (name === "apiosk_inspect_x402") return await handleInspect(argumentsObject, authInfo);
3599
+ if (name === "apiosk_fetch_paid") return await handleFetchPaid(argumentsObject, authInfo);
3039
3600
  if (name === "apiosk_get_api" || name === "apiosk_metadata") {
3040
3601
  return await handleGetApi(argumentsObject, authInfo);
3041
3602
  }
@@ -3054,6 +3615,8 @@ export function createApioskMcpRuntime(options = {}) {
3054
3615
  if (name === "apiosk_wallet_reveal_secret") return await handleLocalWalletReveal(argumentsObject);
3055
3616
  if (name === "apiosk_wallet_save_secret") return await handleLocalWalletSave(argumentsObject);
3056
3617
 
3618
+ if (isPublisherTool(name)) return await handlePublisherTool(name, argumentsObject, authInfo, { env });
3619
+
3057
3620
  if (name === "apiosk_publish_api") return await handlePublishApi(argumentsObject);
3058
3621
  if (name === "apiosk_list_my_apis") return await handleListMyApis(argumentsObject);
3059
3622
  if (name === "apiosk_update_api") return await handleUpdateApi(argumentsObject);
@@ -3080,10 +3643,14 @@ export function createApioskMcpRuntime(options = {}) {
3080
3643
  return await handleDynamicExecute(tool, argumentsObject, authInfo);
3081
3644
  } catch (error) {
3082
3645
  if (error instanceof ApioskPaymentRequiredError) {
3083
- return errorContent({
3084
- error: error.message,
3085
- hint:
3086
- "Run apiosk_get_started in the local stdio package, or configure APIOSK_PRIVATE_KEY, to enable automatic x402 settlement.",
3646
+ // A valid 402 is a business state, not an MCP protocol failure. Returning
3647
+ // isError made ChatGPT/Claude collapse it into JSON-RPC -32603 and hid
3648
+ // the actionable funding guidance from the model.
3649
+ return content({
3650
+ status: "payment_required",
3651
+ error_code: "payment.wallet_unfunded_or_unavailable",
3652
+ message: error.message,
3653
+ hint: buildPaymentRequiredHint(authInfo),
3087
3654
  payment_required: error.paymentRequired,
3088
3655
  });
3089
3656
  }
@@ -3095,7 +3662,19 @@ export function createApioskMcpRuntime(options = {}) {
3095
3662
  }
3096
3663
 
3097
3664
  return {
3098
- listTools: (authInfo = null) => getTools(false, authInfo),
3665
+ listTools: async (authInfo = null) => {
3666
+ const tools = await getTools(false, authInfo);
3667
+ return Promise.all(
3668
+ tools.map(async (tool) =>
3669
+ normalizeToolForClient(tool, {
3670
+ hosted: hostedAuthEnabled,
3671
+ protectedTool: hostedAuthEnabled
3672
+ ? await isToolProtected(tool.name, authInfo)
3673
+ : false,
3674
+ })
3675
+ )
3676
+ );
3677
+ },
3099
3678
  isToolProtected,
3100
3679
  callTool,
3101
3680
  };