@kognai/tool-router 0.0.0-stage → 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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -2
  3. package/dist/adapters/awesome-list.d.ts +10 -0
  4. package/dist/adapters/awesome-list.js +68 -0
  5. package/dist/adapters/mcp-registry.d.ts +12 -0
  6. package/dist/adapters/mcp-registry.js +51 -0
  7. package/dist/adapters/mcp-tools.d.ts +49 -0
  8. package/dist/adapters/mcp-tools.js +60 -0
  9. package/dist/adapters/openapi.d.ts +25 -0
  10. package/dist/adapters/openapi.js +107 -0
  11. package/dist/adapters/pay-skills.d.ts +13 -0
  12. package/dist/adapters/pay-skills.js +119 -0
  13. package/dist/adapters/raw.d.ts +82 -0
  14. package/dist/adapters/raw.js +5 -0
  15. package/dist/adapters/x402-discovery.d.ts +6 -0
  16. package/dist/adapters/x402-discovery.js +100 -0
  17. package/dist/catalog/file-store.d.ts +27 -0
  18. package/dist/catalog/file-store.js +102 -0
  19. package/dist/catalog/ingest.d.ts +53 -0
  20. package/dist/catalog/ingest.js +276 -0
  21. package/dist/catalog/lifecycle.d.ts +24 -0
  22. package/dist/catalog/lifecycle.js +51 -0
  23. package/dist/catalog/memory-store.d.ts +49 -0
  24. package/dist/catalog/memory-store.js +92 -0
  25. package/dist/catalog/risk.d.ts +22 -0
  26. package/dist/catalog/risk.js +73 -0
  27. package/dist/catalog/sanitise.d.ts +17 -0
  28. package/dist/catalog/sanitise.js +109 -0
  29. package/dist/classify/classifier.d.ts +12 -0
  30. package/dist/classify/classifier.js +52 -0
  31. package/dist/classify/decompose.d.ts +10 -0
  32. package/dist/classify/decompose.js +107 -0
  33. package/dist/classify/rules.d.ts +46 -0
  34. package/dist/classify/rules.js +249 -0
  35. package/dist/classify/tagger.d.ts +5 -0
  36. package/dist/classify/tagger.js +29 -0
  37. package/dist/classify/taxonomy.d.ts +31 -0
  38. package/dist/classify/taxonomy.js +221 -0
  39. package/dist/classify/text.d.ts +9 -0
  40. package/dist/classify/text.js +38 -0
  41. package/dist/eligibility/engine.d.ts +11 -0
  42. package/dist/eligibility/engine.js +19 -0
  43. package/dist/eligibility/rules.d.ts +33 -0
  44. package/dist/eligibility/rules.js +121 -0
  45. package/dist/flags.d.ts +33 -0
  46. package/dist/flags.js +34 -0
  47. package/dist/hash.d.ts +8 -0
  48. package/dist/hash.js +30 -0
  49. package/dist/ids.d.ts +17 -0
  50. package/dist/ids.js +54 -0
  51. package/dist/index.d.ts +39 -0
  52. package/dist/index.js +95 -0
  53. package/dist/money.d.ts +10 -0
  54. package/dist/money.js +41 -0
  55. package/dist/rank/confidence.d.ts +15 -0
  56. package/dist/rank/confidence.js +48 -0
  57. package/dist/rank/ranker.d.ts +34 -0
  58. package/dist/rank/ranker.js +81 -0
  59. package/dist/rank/weights.d.ts +40 -0
  60. package/dist/rank/weights.js +46 -0
  61. package/dist/retrieve/bm25.d.ts +21 -0
  62. package/dist/retrieve/bm25.js +59 -0
  63. package/dist/retrieve/capability.d.ts +1 -0
  64. package/dist/retrieve/capability.js +13 -0
  65. package/dist/retrieve/hash-embed.d.ts +4 -0
  66. package/dist/retrieve/hash-embed.js +41 -0
  67. package/dist/retrieve/hybrid.d.ts +74 -0
  68. package/dist/retrieve/hybrid.js +125 -0
  69. package/dist/retrieve/schema-fit.d.ts +9 -0
  70. package/dist/retrieve/schema-fit.js +38 -0
  71. package/dist/retrieve/tokenise.d.ts +2 -0
  72. package/dist/retrieve/tokenise.js +51 -0
  73. package/dist/retrieve/vector.d.ts +18 -0
  74. package/dist/retrieve/vector.js +47 -0
  75. package/dist/router.d.ts +37 -0
  76. package/dist/router.js +114 -0
  77. package/dist/schema/tool.schema.json +117 -0
  78. package/dist/schema/validate.d.ts +9 -0
  79. package/dist/schema/validate.js +92 -0
  80. package/dist/shortlist/generate.d.ts +21 -0
  81. package/dist/shortlist/generate.js +75 -0
  82. package/dist/types/ports.d.ts +71 -0
  83. package/dist/types/ports.js +2 -0
  84. package/dist/types/route.d.ts +121 -0
  85. package/dist/types/route.js +5 -0
  86. package/dist/types/tool.d.ts +123 -0
  87. package/dist/types/tool.js +17 -0
  88. package/package.json +39 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kognai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,48 @@
1
- # Temporary Holding Version
1
+ # @kognai/tool-router
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Kognai ToolRouter (KTR) turns a task written in plain words into a short, ranked list of tools an agent may use.
4
+
5
+ It keeps a catalog of tools built from MCP registries, OpenAPI documents, x402 resource lists and pay-skills
6
+ providers. It classifies the task and drops every tool the caller may not use (missing auth, risk class, payment
7
+ rules, tools nobody has reviewed), then ranks what is left with lexical, vector and capability scores. Every candidate it returns exists in the
8
+ catalog: it never makes a tool up.
9
+
10
+ - No runtime dependencies: Node 18 or later.
11
+ - No third-party code runs. It reads catalogs as data and never installs, launches or imports a tool.
12
+ - Results are data. A shortlist never authorises a payment, a post or a write by itself.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ npm install @kognai/tool-router
18
+ ```
19
+
20
+ ## Use
21
+
22
+ ```ts
23
+ import { MemoryCatalogStore, ingest, tagCapabilities, VERIFY_AUTHORITY, fromOpenApi, RouterIndex, route, readFlags, hashEmbeddingPort } from '@kognai/tool-router';
24
+
25
+ const store = new MemoryCatalogStore();
26
+ const provenance = { source: 'my-openapi', retrieved_at: new Date().toISOString(), licence: 'MIT', snapshot_sha256: '0'.repeat(64) };
27
+ ingest([fromOpenApi(openApiDocument, provenance)], store, { now: new Date().toISOString(), tagger: tagCapabilities });
28
+
29
+ // A tool from outside starts quarantined and is never offered until someone reviews it.
30
+ for (const r of store.latest()) store.setTrust(r.id, r.version, 'reviewed', VERIFY_AUTHORITY, 'reviewed by me');
31
+
32
+ const index = new RouterIndex(store, { vectorModel: hashEmbeddingPort.model });
33
+ const result = await route(
34
+ { text: "What's the weather in Paris tomorrow?", context: { caller_id: 'my-agent', principal_id: 'me', kind: 'engine', business_tier: 'enterprise', connected: [], kognai_first: 'off' } },
35
+ { store, index, flags: readFlags({}), offline: true, embed: hashEmbeddingPort },
36
+ );
37
+
38
+ console.log(result.requirement, result.shortlists); // 'tool', with the forecast operation in the shortlist
39
+ ```
40
+
41
+ `offline: true` runs the router without the `TOOL_ROUTER_ENABLED` switch. Online, the router answers only when that
42
+ switch is on, and a stop flag always wins.
43
+
44
+ The bundled hash embedding works without a model. To use your own embedding model, pass an `EmbeddingPort`.
45
+
46
+ ## Licence
47
+
48
+ MIT
@@ -0,0 +1,10 @@
1
+ /**
2
+ * awesome-mcp-servers (github punkpeye/awesome-mcp-servers, MIT list; each server under its own licence) Markdown
3
+ * lines to metadata only descriptors. Lifecycle "draft": never eligible, not even in discover mode; they exist in the
4
+ * catalog so the router can say that unverified community servers exist (exclusion counts) and so the benchmark has
5
+ * realistic volume. The emoji legend becomes tags.
6
+ */
7
+ import type { ProvenanceIn, RawToolDescriptor } from './raw';
8
+ export declare const ADAPTER = "awesome-list";
9
+ export declare const ADAPTER_VERSION = "1.0.0";
10
+ export declare function fromAwesomeMarkdown(md: string, p: ProvenanceIn): RawToolDescriptor[];
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ADAPTER_VERSION = exports.ADAPTER = void 0;
4
+ exports.fromAwesomeMarkdown = fromAwesomeMarkdown;
5
+ const raw_1 = require("./raw");
6
+ exports.ADAPTER = 'awesome-list';
7
+ exports.ADAPTER_VERSION = '1.0.0';
8
+ const LEGEND = [
9
+ ['🎖️', 'official'], ['🎖', 'official'], ['📇', 'typescript'], ['🐍', 'python'], ['🏎️', 'go'], ['🏎', 'go'], ['🦀', 'rust'],
10
+ ['#️⃣', 'csharp'], ['☕', 'java'], ['🌊', 'c-cpp'], ['💎', 'ruby'], ['☁️', 'cloud'], ['☁', 'cloud'], ['🏠', 'local'],
11
+ ['📟', 'embedded'], ['🍎', 'macos'], ['🪟', 'windows'], ['🐧', 'linux'],
12
+ ];
13
+ const LINE = /^\s*[-*]\s+\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)(.*)$/;
14
+ const HEADING = /^###\s+(.*)$/;
15
+ function fromAwesomeMarkdown(md, p) {
16
+ const out = [];
17
+ const used = new Set();
18
+ let category = 'uncategorised';
19
+ let inServers = false;
20
+ for (const line of md.split(/\r?\n/)) {
21
+ if (/^##\s/.test(line)) {
22
+ inServers = /^##\s+Server Implementations/i.test(line);
23
+ continue;
24
+ }
25
+ const h = HEADING.exec(line);
26
+ if (h) {
27
+ const anchor = /<a name="([^"]+)"/.exec(h[1]);
28
+ category = (anchor ? anchor[1] : h[1].replace(/<[^>]+>/g, '').replace(/[^\p{L}\p{N} &-]/gu, '').trim()).toLowerCase().replace(/\s+/g, '-') || 'uncategorised';
29
+ continue;
30
+ }
31
+ if (!inServers)
32
+ continue;
33
+ const m = LINE.exec(line);
34
+ if (!m)
35
+ continue;
36
+ const [, label, url, rest] = m;
37
+ const gh = /github\.com\/([^/\s]+)\/([^/\s#?]+)/i.exec(url);
38
+ let host = 'unknown';
39
+ let segs = [];
40
+ try {
41
+ const u = new URL(url);
42
+ host = u.hostname;
43
+ segs = u.pathname.split('/').filter(Boolean);
44
+ }
45
+ catch { /* keep unknown */ }
46
+ const ns = gh ? gh[1] : segs[0] || host;
47
+ const nm = (gh ? gh[2] : segs[1] || label.replace(/^@/, '').split('/').pop() || label).replace(/\.git$/, '');
48
+ const providerName = gh ? gh[1] : host;
49
+ let path = `${ns}/${nm}`;
50
+ for (let i = 2; used.has(path.toLowerCase()); i++)
51
+ path = `${ns}/${nm}-${i}`;
52
+ used.add(path.toLowerCase());
53
+ const noBadges = rest.replace(/\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)/g, ' ').replace(/!\[[^\]]*\]\([^)]*\)/g, ' ');
54
+ const dash = noBadges.search(/\s[-–—]\s/);
55
+ const legendPart = dash >= 0 ? noBadges.slice(0, dash) : noBadges;
56
+ const description = dash >= 0 ? noBadges.slice(dash).replace(/^\s*[-–—]\s*/, '').trim() : '';
57
+ const tags = [...new Set(LEGEND.filter(([e]) => legendPart.includes(e)).map(([, t]) => t))];
58
+ out.push({
59
+ source: 'awesome-mcp-servers', source_ref: url, origin: 'community', path,
60
+ name: label.slice(0, 120), description,
61
+ provider: { name: providerName, url, kognai_owned: false }, category, tags: ['community', 'metadata-only', category, ...tags],
62
+ operations: [{ op: 'server', summary: description || label, input_schema: null, output_schema: null, side_effects: null }],
63
+ auth: { kind: 'none' }, pricing: { model: 'unknown', source: 'unknown' }, licence: null,
64
+ implementation: 'metadata', lifecycle: 'draft', provenance: (0, raw_1.prov)(p, exports.ADAPTER, exports.ADAPTER_VERSION),
65
+ });
66
+ }
67
+ return out;
68
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * MCP registry server.json entries (registry.modelcontextprotocol.io, `GET /v0/servers` pages) to raw descriptors.
3
+ * Metadata only: the registry does not list a server's tools, so each server becomes one tool with one unknown
4
+ * operation ("server"), unknown schema and unknown side effects (fail closed). Servers that ship only `packages` are
5
+ * marked implementation "package" and are never runnable by KTR; `remotes` make a server a candidate for a later
6
+ * CTO desk verification. Nothing here installs or launches anything.
7
+ */
8
+ import type { ProvenanceIn, RawToolDescriptor } from './raw';
9
+ export declare const ADAPTER = "mcp-registry";
10
+ export declare const ADAPTER_VERSION = "1.0.0";
11
+ /** Accepts a registry page ({servers: [...]}), an array of entries, or one server.json. */
12
+ export declare function fromServerJson(input: unknown, p: ProvenanceIn): RawToolDescriptor[];
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ADAPTER_VERSION = exports.ADAPTER = void 0;
4
+ exports.fromServerJson = fromServerJson;
5
+ const raw_1 = require("./raw");
6
+ exports.ADAPTER = 'mcp-registry';
7
+ exports.ADAPTER_VERSION = '1.0.0';
8
+ const NAME_RE = /^[a-zA-Z0-9.-]+\/[a-zA-Z0-9._-]+$/;
9
+ /** Accepts a registry page ({servers: [...]}), an array of entries, or one server.json. */
10
+ function fromServerJson(input, p) {
11
+ const list = Array.isArray(input) ? input
12
+ : (input && typeof input === 'object' && Array.isArray(input.servers)) ? input.servers
13
+ : [input];
14
+ const out = [];
15
+ for (const e of list) {
16
+ const s = (e && e.server) || e;
17
+ if (!s || typeof s !== 'object' || typeof s.name !== 'string' || !NAME_RE.test(s.name))
18
+ continue;
19
+ const official = e?._meta?.['io.modelcontextprotocol.registry/official'] || {};
20
+ // the registry lists every published version; only the latest one enters the catalog
21
+ if (official.isLatest === false)
22
+ continue;
23
+ const status = String(official.status || 'active');
24
+ const remotes = (s.remotes || []).filter((r) => typeof r?.url === 'string' && /^https:\/\//i.test(r.url));
25
+ const pkg = (s.packages || []).find((x) => x && (x.identifier || x.name));
26
+ const implementation = remotes.length ? 'remote' : pkg ? 'package' : 'metadata';
27
+ const [ns, nm] = s.name.split('/');
28
+ out.push({
29
+ source: 'mcp-registry',
30
+ source_ref: `${s.name}@${s.version || '?'}`,
31
+ origin: 'mcp',
32
+ path: `${ns}/${nm}`,
33
+ name: (s.title || nm).slice(0, 120),
34
+ description: String(s.description || ''),
35
+ provider: { name: ns, url: s.websiteUrl || s.repository?.url, kognai_owned: false },
36
+ category: 'mcp-server',
37
+ tags: ['mcp', implementation],
38
+ operations: [{ op: 'server', summary: String(s.description || s.title || nm), input_schema: null, output_schema: null, side_effects: null }],
39
+ auth: { kind: 'none' },
40
+ pricing: { model: 'unknown', source: 'unknown' },
41
+ licence: p.licence,
42
+ endpoint: remotes[0]?.url,
43
+ package: pkg ? { registry: String(pkg.registryType || pkg.registry_name || 'unknown'), identifier: String(pkg.identifier || pkg.name), version: pkg.version } : undefined,
44
+ implementation,
45
+ version: s.version,
46
+ lifecycle: status === 'deleted' ? 'retired' : status === 'deprecated' ? 'deprecated' : 'active',
47
+ provenance: (0, raw_1.prov)(p, exports.ADAPTER, exports.ADAPTER_VERSION),
48
+ });
49
+ }
50
+ return out;
51
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * MCP tools/list results to raw descriptors, for APPROVED servers only (policy/tool-router.txt rule 2).
3
+ *
4
+ * fromMcpToolsList() reads a tools/list result that is already a file (a fixture in tests, a pinned snapshot
5
+ * otherwise). listToolsLive() is the only path that talks to a server, and it refuses unless all of these hold: the
6
+ * flag TOOL_ROUTER_MCP_LIST_LIVE is on, the server is in the approved list (workspace/ktr/mcp-approved.json, the CTO
7
+ * desk's file, decision D10), and the server has an https remote endpoint. A package-only server is refused always:
8
+ * KTR never installs or launches third party code, so there is no stdio transport here at all.
9
+ *
10
+ * Tool annotations (readOnlyHint, destructiveHint) come from the server and are untrusted: a destructive hint adds
11
+ * risk; a read only hint is believed only when the approval entry says the CTO desk checked it (trust_annotations).
12
+ */
13
+ import type { KtrFlags } from '../flags';
14
+ import type { ProvenanceIn, RawToolDescriptor } from './raw';
15
+ export declare const ADAPTER = "mcp-tools";
16
+ export declare const ADAPTER_VERSION = "1.0.0";
17
+ export interface ApprovedServer {
18
+ name: string;
19
+ remote_url?: string;
20
+ approved_by: string;
21
+ approved_at: string;
22
+ schema_hash?: string;
23
+ trust_annotations?: boolean;
24
+ }
25
+ export interface McpTool {
26
+ name: string;
27
+ title?: string;
28
+ description?: string;
29
+ inputSchema?: Record<string, unknown>;
30
+ outputSchema?: Record<string, unknown>;
31
+ annotations?: {
32
+ readOnlyHint?: boolean;
33
+ destructiveHint?: boolean;
34
+ idempotentHint?: boolean;
35
+ openWorldHint?: boolean;
36
+ };
37
+ }
38
+ export declare class NotApproved extends Error {
39
+ }
40
+ export declare function approvedEntry(name: string, approved: ApprovedServer[]): ApprovedServer | null;
41
+ export declare function fromMcpToolsList(serverName: string, result: unknown, approved: ApprovedServer[], p: ProvenanceIn): RawToolDescriptor;
42
+ /** JSON-RPC over an https remote; injected so tests never touch the network. */
43
+ export type McpHttpTransport = (url: string, body: {
44
+ jsonrpc: '2.0';
45
+ id: number;
46
+ method: 'tools/list';
47
+ params: Record<string, unknown>;
48
+ }) => Promise<unknown>;
49
+ export declare function listToolsLive(serverName: string, approved: ApprovedServer[], flags: Pick<KtrFlags, 'mcpListLive' | 'stopped'>, transport: McpHttpTransport): Promise<unknown>;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NotApproved = exports.ADAPTER_VERSION = exports.ADAPTER = void 0;
4
+ exports.approvedEntry = approvedEntry;
5
+ exports.fromMcpToolsList = fromMcpToolsList;
6
+ exports.listToolsLive = listToolsLive;
7
+ const raw_1 = require("./raw");
8
+ exports.ADAPTER = 'mcp-tools';
9
+ exports.ADAPTER_VERSION = '1.0.0';
10
+ class NotApproved extends Error {
11
+ }
12
+ exports.NotApproved = NotApproved;
13
+ function approvedEntry(name, approved) {
14
+ return approved.find((a) => a.name === name && a.approved_by === 'cto-desk') || null;
15
+ }
16
+ function sideEffectsOf(t, a) {
17
+ if (t.annotations?.destructiveHint === true)
18
+ return ['delete'];
19
+ if (t.annotations?.readOnlyHint === true && a.trust_annotations === true)
20
+ return ['read'];
21
+ return null;
22
+ }
23
+ function fromMcpToolsList(serverName, result, approved, p) {
24
+ const a = approvedEntry(serverName, approved);
25
+ if (!a)
26
+ throw new NotApproved(`${serverName} is not in the approved MCP list (CTO desk, D10)`);
27
+ const tools = Array.isArray(result?.tools) ? result.tools : [];
28
+ if (tools.length > 200)
29
+ throw new Error(`${serverName} lists ${tools.length} tools, more than 200 (T14)`);
30
+ const ops = tools.filter((t) => t && typeof t.name === 'string').map((t) => ({
31
+ op: t.name,
32
+ summary: String(t.description || t.title || t.name),
33
+ input_schema: (t.inputSchema && typeof t.inputSchema === 'object') ? t.inputSchema : null,
34
+ output_schema: (t.outputSchema && typeof t.outputSchema === 'object') ? t.outputSchema : null,
35
+ side_effects: sideEffectsOf(t, a),
36
+ idempotent: t.annotations?.idempotentHint === true && a.trust_annotations === true ? true : null,
37
+ }));
38
+ const [ns, nm] = serverName.split('/');
39
+ return {
40
+ source: 'mcp-tools', source_ref: serverName, origin: 'mcp', path: `${ns}/${nm}`,
41
+ name: nm, description: ops.map((o) => o.summary).join(' ').slice(0, 2000),
42
+ provider: { name: ns, kognai_owned: false }, category: 'mcp-server', tags: ['mcp', 'remote', 'tools-listed'],
43
+ operations: ops, auth: { kind: 'none' }, pricing: { model: 'unknown', source: 'unknown' },
44
+ licence: p.licence, endpoint: a.remote_url, implementation: 'remote', lifecycle: 'active',
45
+ provenance: (0, raw_1.prov)(p, exports.ADAPTER, exports.ADAPTER_VERSION),
46
+ };
47
+ }
48
+ async function listToolsLive(serverName, approved, flags, transport) {
49
+ if (flags.stopped)
50
+ throw new Error('KTR is stopped (.swarm-state/ktr-stop)');
51
+ if (!flags.mcpListLive)
52
+ throw new Error('TOOL_ROUTER_MCP_LIST_LIVE is off');
53
+ const a = approvedEntry(serverName, approved);
54
+ if (!a)
55
+ throw new NotApproved(`${serverName} is not in the approved MCP list (CTO desk, D10)`);
56
+ if (!a.remote_url || !/^https:\/\//i.test(a.remote_url))
57
+ throw new Error(`${serverName} has no https remote: KTR never launches a package (policy/tool-router.txt rule 2)`);
58
+ const res = await transport(a.remote_url, { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} });
59
+ return res?.result ?? res;
60
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * OpenAPI 3.0 and 3.1 documents to one raw descriptor with one operation per path and method. The input schema is
3
+ * built from parameters plus the JSON request body; local $ref pointers are resolved (depth and size capped, cycles
4
+ * cut). Side effects come from the method and fail closed: GET and HEAD read, POST, PUT and PATCH write, DELETE
5
+ * deletes. A POST is read only when the caller's own reviewed list says so (`safePostOps`), never because the
6
+ * provider's text claims it.
7
+ */
8
+ import type { SideEffect } from '../types/tool';
9
+ import type { ProvenanceIn, RawToolDescriptor } from './raw';
10
+ export declare const ADAPTER = "openapi";
11
+ export declare const ADAPTER_VERSION = "1.0.0";
12
+ export interface OpenApiOpts {
13
+ /** namespace/name for the tool id; defaults to the server host and the title. */
14
+ path?: string;
15
+ origin?: 'openapi' | 'x402';
16
+ provider?: {
17
+ name: string;
18
+ url?: string;
19
+ };
20
+ safePostOps?: string[];
21
+ category?: string;
22
+ tags?: string[];
23
+ }
24
+ export declare function sideEffectOf(method: string, op: string, safePostOps?: string[]): SideEffect[];
25
+ export declare function fromOpenApi(doc: any, p: ProvenanceIn, opts?: OpenApiOpts): RawToolDescriptor;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ADAPTER_VERSION = exports.ADAPTER = void 0;
4
+ exports.sideEffectOf = sideEffectOf;
5
+ exports.fromOpenApi = fromOpenApi;
6
+ const raw_1 = require("./raw");
7
+ exports.ADAPTER = 'openapi';
8
+ exports.ADAPTER_VERSION = '1.0.0';
9
+ const METHODS = ['get', 'head', 'post', 'put', 'patch', 'delete'];
10
+ function resolver(doc) {
11
+ const lookup = (ref) => {
12
+ if (!ref.startsWith('#/'))
13
+ return null;
14
+ return ref.slice(2).split('/').reduce((o, k) => (o == null ? o : o[k.replace(/~1/g, '/').replace(/~0/g, '~')]), doc);
15
+ };
16
+ // `hops` counts $ref follows (cap 8), `level` counts nesting (cap 32); a ref already on the path is a cycle.
17
+ const resolve = (s, hops = 0, seen = new Set(), level = 0) => {
18
+ if (!s || typeof s !== 'object')
19
+ return s;
20
+ if (hops > 8 || level > 32)
21
+ return {};
22
+ if (typeof s.$ref === 'string') {
23
+ if (seen.has(s.$ref))
24
+ return { type: 'object', description: 'recursive' };
25
+ const t = lookup(s.$ref);
26
+ return t ? resolve(t, hops + 1, new Set([...seen, s.$ref]), level + 1) : {};
27
+ }
28
+ if (Array.isArray(s))
29
+ return s.map((x) => resolve(x, hops, seen, level + 1));
30
+ const out = {};
31
+ for (const [k, v] of Object.entries(s))
32
+ out[k] = typeof v === 'object' ? resolve(v, hops, seen, level + 1) : v;
33
+ return out;
34
+ };
35
+ return resolve;
36
+ }
37
+ const opName = (method, path, operationId) => (operationId || `${method}_${path}`).replace(/[^A-Za-z0-9_.-]+/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '').toLowerCase().slice(0, 64) || method;
38
+ function sideEffectOf(method, op, safePostOps = []) {
39
+ switch (method) {
40
+ case 'get':
41
+ case 'head': return ['read'];
42
+ case 'delete': return ['delete'];
43
+ case 'post': return safePostOps.includes(op) ? ['read'] : ['write'];
44
+ default: return ['write'];
45
+ }
46
+ }
47
+ function fromOpenApi(doc, p, opts = {}) {
48
+ if (!doc || typeof doc !== 'object' || !/^3\./.test(String(doc.openapi || '')))
49
+ throw new Error('not an OpenAPI 3.x document');
50
+ const resolve = resolver(doc);
51
+ const server = String(doc.servers?.[0]?.url || '');
52
+ let host = 'unknown';
53
+ try {
54
+ host = new URL(server).hostname;
55
+ }
56
+ catch { /* relative server */ }
57
+ const title = String(doc.info?.title || host);
58
+ const ops = [];
59
+ for (const [path, item] of Object.entries(doc.paths || {})) {
60
+ const shared = Array.isArray(item?.parameters) ? item.parameters : [];
61
+ for (const method of METHODS) {
62
+ const o = item?.[method];
63
+ if (!o || typeof o !== 'object')
64
+ continue;
65
+ const op = opName(method, path, o.operationId);
66
+ const properties = {};
67
+ const required = [];
68
+ for (const prm of [...shared, ...(Array.isArray(o.parameters) ? o.parameters : [])].map((x) => resolve(x))) {
69
+ if (!prm || typeof prm.name !== 'string' || prm.in === 'header' || prm.in === 'cookie')
70
+ continue;
71
+ properties[prm.name] = { ...(resolve(prm.schema) || { type: 'string' }), ...(prm.description ? { description: String(prm.description).slice(0, 200) } : {}) };
72
+ if (prm.required || prm.in === 'path')
73
+ required.push(prm.name);
74
+ }
75
+ const body = resolve(o.requestBody);
76
+ const bodySchema = body?.content?.['application/json']?.schema;
77
+ if (bodySchema) {
78
+ properties.body = resolve(bodySchema);
79
+ if (body.required)
80
+ required.push('body');
81
+ }
82
+ const ok = o.responses?.['200'] || o.responses?.['201'] || o.responses?.default;
83
+ const outSchema = resolve(ok)?.content?.['application/json']?.schema;
84
+ ops.push({
85
+ op, method: method.toUpperCase(), path,
86
+ summary: String(o.summary || o.description || `${method.toUpperCase()} ${path}`),
87
+ input_schema: { type: 'object', properties, ...(required.length ? { required: [...new Set(required)] } : {}) },
88
+ output_schema: outSchema ? resolve(outSchema) : null,
89
+ side_effects: sideEffectOf(method, op, opts.safePostOps),
90
+ idempotent: method === 'get' || method === 'head' || method === 'put' || method === 'delete',
91
+ });
92
+ }
93
+ }
94
+ return {
95
+ source: p.source, source_ref: `${title}@${doc.info?.version || '?'}`,
96
+ origin: opts.origin || 'openapi',
97
+ path: opts.path || `${host}/${title}`,
98
+ name: title.slice(0, 120),
99
+ description: String(doc.info?.description || title),
100
+ provider: { name: opts.provider?.name || host, url: opts.provider?.url || server || undefined, kognai_owned: false },
101
+ category: opts.category || 'api', tags: ['openapi', ...(opts.tags || [])],
102
+ operations: ops, auth: { kind: doc.components?.securitySchemes ? 'api_key' : 'none' },
103
+ pricing: { model: 'unknown', source: 'unknown' }, licence: p.licence,
104
+ endpoint: server || undefined, implementation: 'http', version: doc.info?.version ? String(doc.info.version) : undefined,
105
+ lifecycle: 'active', provenance: (0, raw_1.prov)(p, exports.ADAPTER, exports.ADAPTER_VERSION),
106
+ };
107
+ }
@@ -0,0 +1,13 @@
1
+ import type { ProvenanceIn, RawToolDescriptor } from './raw';
2
+ export declare const ADAPTER = "pay-skills";
3
+ export declare const ADAPTER_VERSION = "1.0.0";
4
+ export interface PayMdFile {
5
+ path: string;
6
+ md: string;
7
+ openapi?: unknown;
8
+ }
9
+ /** The front matter subset pay-skills uses: top level scalars and one level of nested scalars. */
10
+ export declare function frontMatter(md: string): Record<string, any> | null;
11
+ /** '$0.01' or '0.01' to micro units; null when it is not a plain USD amount. */
12
+ export declare function priceMicro(s: unknown): bigint | null;
13
+ export declare function fromPaySkills(files: PayMdFile[], p: ProvenanceIn): RawToolDescriptor[];
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ADAPTER_VERSION = exports.ADAPTER = void 0;
4
+ exports.frontMatter = frontMatter;
5
+ exports.priceMicro = priceMicro;
6
+ exports.fromPaySkills = fromPaySkills;
7
+ const money_1 = require("../money");
8
+ const openapi_1 = require("./openapi");
9
+ const raw_1 = require("./raw");
10
+ exports.ADAPTER = 'pay-skills';
11
+ exports.ADAPTER_VERSION = '1.0.0';
12
+ /** The front matter subset pay-skills uses: top level scalars and one level of nested scalars. */
13
+ function frontMatter(md) {
14
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(md);
15
+ if (!m)
16
+ return null;
17
+ const out = {};
18
+ let parent = null;
19
+ const unq = (v) => {
20
+ const t = v.trim();
21
+ if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
22
+ try {
23
+ return t.startsWith('"') ? JSON.parse(t) : t.slice(1, -1);
24
+ }
25
+ catch {
26
+ return t.slice(1, -1);
27
+ }
28
+ }
29
+ return t;
30
+ };
31
+ for (const line of m[1].split(/\r?\n/)) {
32
+ const top = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
33
+ const nested = /^\s{2,}([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
34
+ if (top) {
35
+ if (top[2] === '') {
36
+ out[top[1]] = {};
37
+ parent = top[1];
38
+ }
39
+ else {
40
+ out[top[1]] = unq(top[2]);
41
+ parent = null;
42
+ }
43
+ }
44
+ else if (nested && parent) {
45
+ out[parent][nested[1]] = unq(nested[2]);
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+ /** '$0.01' or '0.01' to micro units; null when it is not a plain USD amount. */
51
+ function priceMicro(s) {
52
+ const m = /^\s*\$?\s*(\d+(?:\.\d{1,6})?)\s*$/.exec(String(s ?? ''));
53
+ if (!m)
54
+ return null;
55
+ try {
56
+ return (0, money_1.toMicro)(m[1]);
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ function networksOf(text) {
63
+ const n = [];
64
+ if (/\bbase\b/i.test(text))
65
+ n.push('base');
66
+ if (/\bsolana\b/i.test(text))
67
+ n.push('solana');
68
+ return n;
69
+ }
70
+ function fromPaySkills(files, p) {
71
+ const out = [];
72
+ for (const f of files) {
73
+ const fm = frontMatter(f.md);
74
+ const parts = /(?:^|\/)providers\/(.+)\/PAY\.md$/.exec(f.path);
75
+ if (!fm || !parts || !fm.name)
76
+ continue;
77
+ const segs = parts[1].split('/');
78
+ if (segs.length < 2)
79
+ continue;
80
+ const operator = segs[0];
81
+ const name = segs.slice(1).join('-');
82
+ const body = f.md.replace(/^---[\s\S]*?---/, '');
83
+ const description = [fm.description, fm.use_case].filter(Boolean).join(' ');
84
+ let ops = [{ op: 'call', summary: String(fm.description || fm.title || name), input_schema: null, output_schema: null, side_effects: null }];
85
+ let pricing = { model: 'unknown', source: 'unknown' };
86
+ const networks = networksOf(body);
87
+ if (f.openapi) {
88
+ try {
89
+ const d = (0, openapi_1.fromOpenApi)(f.openapi, p, { origin: 'x402', path: `${operator}/${name}` });
90
+ if (d.operations.length)
91
+ ops = d.operations;
92
+ const prices = [];
93
+ for (const [, item] of Object.entries(f.openapi.paths || {})) {
94
+ for (const o of Object.values(item || {})) {
95
+ const pm = priceMicro(o?.['x-payment-info']?.price);
96
+ if (pm !== null)
97
+ prices.push(pm);
98
+ }
99
+ }
100
+ if (prices.length) {
101
+ const min = prices.reduce((a, b) => (b < a ? b : a));
102
+ pricing = { model: 'per_call', price: { amount_micro: min.toString(), currency: 'USDC', ...(networks[0] ? { network: networks[0] } : {}) }, unit: 'request', source: 'provider' };
103
+ }
104
+ }
105
+ catch { /* a broken openapi keeps the single unknown operation */ }
106
+ }
107
+ const priceLine = /\$\d+(?:\.\d+)?(?:\s*(?:per|\/)\s*\w+)?/.exec(body)?.[0];
108
+ out.push({
109
+ source: 'pay-skills', source_ref: f.path, origin: 'x402', path: `${operator}/${name}`,
110
+ name: String(fm.title || name).slice(0, 120), description,
111
+ provider: { name: operator, url: fm.service_url, kognai_owned: false },
112
+ category: String(fm.category || 'other'), tags: ['pay-skills', 'x402', String(fm.category || 'other')],
113
+ operations: ops, auth: { kind: 'x402' }, pricing, pricing_text: priceLine,
114
+ licence: p.licence, endpoint: fm.service_url, implementation: 'http', version: fm.version ? String(fm.version) : undefined,
115
+ networks, lifecycle: 'active', provenance: (0, raw_1.prov)(p, exports.ADAPTER, exports.ADAPTER_VERSION),
116
+ });
117
+ }
118
+ return out;
119
+ }