keymaker-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aditya Acharya
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 ADDED
@@ -0,0 +1,184 @@
1
+
2
+
3
+
4
+ # Keymaker
5
+
6
+ **Make your API agent-ready in an afternoon.**
7
+
8
+
9
+
10
+ https://github.com/user-attachments/assets/a8e5b189-dbaa-45dc-81b4-af4f2b72c85b
11
+
12
+
13
+
14
+ Point Keymaker at your OpenAPI spec. It emits everything an AI agent needs to discover, sign up for, and use your product — with zero humans in the loop:
15
+
16
+ - **`mcp-server.mjs`** — a runnable MCP server exposing every endpoint as a tool (stdio; works with Claude, Cursor, any MCP client)
17
+ - **`llms.txt`** — agent-readable API overview, served the way agents actually browse
18
+ - **`auth.md`** + **`.well-known/auth.md`** — machine-readable signup instructions ([auth.md](https://workos.com/auth-md)-pattern-compatible)
19
+ - **an agent-signup server** — agents `POST /agent-auth` and get a scoped API key back in one round trip; your backend verifies keys and meters usage with one call
20
+
21
+ Plus a **curation report**: every endpoint or parameter with missing/weak documentation gets flagged — because agents choose tools by documentation quality, and auto-generated tools from an undocumented spec perform measurably worse.
22
+
23
+ ## Quickstart
24
+
25
+ ```bash
26
+ npx keymaker-cli generate https://your.api/openapi.json -o agent-ready
27
+ npx keymaker-cli serve agent-ready # agent signup + hosted /mcp on :8787
28
+ ```
29
+
30
+ Or from a clone:
31
+
32
+ ```bash
33
+ npm install
34
+ node src/cli.js generate examples/todo-api.yaml -o out/todo
35
+ node src/cli.js serve out/todo # agent signup on :8787
36
+ node out/todo/mcp-server.mjs # MCP server (stdio)
37
+ ```
38
+
39
+ An agent signs itself up:
40
+
41
+ ```bash
42
+ curl -s -X POST localhost:8787/agent-auth \
43
+ -d '{"client_name":"my-agent","scopes":["read","write"]}'
44
+ # → { "api_key": "ak_…", "status": "unclaimed", "expires_at": "<60 min>", "claim_token": "ct_…" }
45
+ ```
46
+
47
+ No attestation → the key is **temporary (60 min)** — enough for the agent to try your API — and becomes permanent when a human claims it (the [Cloudflare Temporary Accounts](https://blog.cloudflare.com/temporary-accounts/) pattern). With a platform attestation (ID-JAG) → issued as `agent_verified`, no expiry. Your backend checks keys and meters usage:
48
+
49
+ ```bash
50
+ curl -s -X POST localhost:8787/agent-auth/verify -d '{"api_key":"ak_…"}'
51
+ # → { "valid": true, "scopes": ["read","write"], "usage": 1 }
52
+ ```
53
+
54
+ ## Mount it inside your app (one process, one port)
55
+
56
+ You don't need a second service. Mount the gateway in your existing app:
57
+
58
+ ```js
59
+ import { keymakerGateway, keymakerAuth } from "keymaker-cli";
60
+
61
+ const gateway = await keymakerGateway({ dir: "./agent-ready" });
62
+ const auth = keymakerAuth({ dir: "./agent-ready" });
63
+
64
+ // plain node:http
65
+ createServer(async (req, res) => {
66
+ if (await gateway(req, res)) return; // /llms.txt /auth.md /agent-auth* /mcp
67
+ if (!(await auth(req, res))) return; // everything else needs an agent key
68
+ // …your routes, with req.agent populated
69
+ });
70
+
71
+ // express
72
+ app.use(gateway.express());
73
+ app.use(auth);
74
+ ```
75
+
76
+ `node examples/single-process.mjs` shows the whole thing on one port.
77
+
78
+ ## Agents as paying customers (Stripe)
79
+
80
+ Add to `signup.config.json` and set `STRIPE_SECRET_KEY`:
81
+
82
+ ```json
83
+ "billing": { "provider": "stripe", "meter_event_name": "keymaker_api_call" }
84
+ ```
85
+
86
+ Registration creates a Stripe customer for the agent (pass `billing_email` at signup — agents have inboxes now); every metered request emits a [Billing Meter event](https://docs.stripe.com/billing/subscriptions/usage-based). Attach a metered price and usage becomes invoices. Billing is optional and off by default — without config, nothing changes.
87
+
88
+ ## One origin, whole loop: hosted `/mcp`
89
+
90
+ `keymaker serve` doesn't just issue keys — it hosts the tools too. An agent needs exactly one URL:
91
+
92
+ ```
93
+ GET /llms.txt → discovers your API
94
+ GET /auth.md → learns how to register
95
+ POST /agent-auth → gets a scoped key
96
+ POST /mcp → calls your API's tools (Streamable HTTP, Bearer-gated)
97
+ ```
98
+
99
+ No stdio, no local install. Tool visibility is scope-filtered — a `read`-scoped key doesn't even *see* write tools in `tools/list`. Every successful call is metered onto the key. Revoke instantly:
100
+
101
+ ```bash
102
+ curl -X POST localhost:8787/agent-auth/revoke \
103
+ -H "x-admin-token: <from signup.config.json>" -d '{"key_id":"key_…"}'
104
+ ```
105
+
106
+ (The generated `mcp-server.mjs` still works for stdio/local MCP clients like Claude Desktop.)
107
+
108
+ ## Check any live site: `keymaker doctor`
109
+
110
+ ```bash
111
+ npx keymaker-cli doctor stripe.com yourapi.com
112
+ # stripe.com: 1/3 agent surfaces llms.txt ✓ auth.md ✗ /mcp ✗
113
+ ```
114
+
115
+ We ran it across 63 top API companies: **71% have llms.txt, 8% have agent signup, exactly one has all three.** Full data: [State of Agent Readiness, July 2026](docs/state-of-agent-readiness-2026-07.md).
116
+
117
+ ## Score your API (Lighthouse, for agents)
118
+
119
+ ```bash
120
+ node src/cli.js score https://petstore3.swagger.io/api/v3/openapi.json
121
+ # Agent-readiness: 84/100 grade B
122
+ # ██████████ 40/40 Operation docs
123
+ # ░░░░░░░░░░ 0/10 Absolute base URL → servers[0].url should be an absolute https URL
124
+ # …
125
+ ```
126
+
127
+ Six checks, weighted by what actually drives agent tool-selection: operation docs, parameter docs, API description, absolute base URL, documented auth, tagging. Fun fact: the official Swagger Petstore scores a B — its base URL is relative, so an agent literally can't call it.
128
+
129
+ ## Protect your API (drop-in middleware)
130
+
131
+ ```js
132
+ import { keymakerAuth } from "keymaker-cli";
133
+
134
+ const auth = keymakerAuth({ dir: "./agent-ready", rateLimitPerMinute: 60 });
135
+ app.use(auth); // Express
136
+ // or in plain node:http — if (!(await auth(req, res))) return;
137
+ // req.agent = { key_id, client_name, scopes, status }
138
+ ```
139
+
140
+ Checks the Bearer key, enforces scopes (`read` for GET, `write` for mutations), rate-limits per key, meters usage.
141
+
142
+ ## Real attestation verification
143
+
144
+ `signup.config.json` (generated next to your artifacts) controls how `agent_verified` keys are issued:
145
+
146
+ ```json
147
+ {
148
+ "trusted_issuers": [{ "issuer": "https://agents.example.com", "jwks_url": "https://agents.example.com/.well-known/jwks.json" }],
149
+ "audience": "https://api.yourproduct.com",
150
+ "registrations_per_minute_per_ip": 10
151
+ }
152
+ ```
153
+
154
+ Attestations are verified as JWTs (RS256/ES256/EdDSA) against the issuer's JWKS — signature, expiry, issuer, and audience checks. The generated default is dev-mode (`dev_accept_any_attestation: true`) so the demo works out of the box; a rejected attestation still issues a 60-minute temporary key, with the rejection reason in the response.
155
+
156
+ ## Why now
157
+
158
+ - Agents are becoming the first consumer of APIs, and they pick tools by machine-readability. On YC's Lightcone podcast (Feb 2026), Garry Tan described Claude Code choosing the practically-deprecated Whisper V1 over Groq — 200× faster, 10× cheaper — for his transcription pipeline; his co-host pinned the cause: Groq's docs are hard to parse, Whisper's have far more examples.
159
+ - YC's Summer 2026 RFS calls for exactly this: agents need to "discover, sign up for, and instantly start using new tools programmatically, without needing a human in the loop."
160
+ - The signup protocol layer just standardized (WorkOS's auth.md, May 2026 — adopted by Cloudflare, Firecrawl, Resend, Monday.com). But implementing it — endpoints, attestation verification, key issuance, metering — is still work every vendor does by hand. **Keymaker is the retrofit layer: one command from OpenAPI spec to fully agent-ready.**
161
+
162
+ ## Security posture
163
+
164
+ - **Keys are hashed at rest** (SHA-256) — `keys.json` stores hash + display prefix; the full key is shown exactly once, at registration. Lookups are timing-safe.
165
+ - **Admin token comparison is timing-safe**; key writes are serialized and land atomically (tmp + rename).
166
+ - **Rate limits by default**: 10 registrations/min/IP, 120 verifies/min/key, 60 API requests/min/key — all configurable.
167
+ - Attestations are verified as signed JWTs against the issuer's JWKS (see above); dev-mode acceptance is explicit, config-gated, and off unless you say otherwise.
168
+
169
+ Scale note: real-world specs are fine — GitHub's 12.6MB / 1,194-operation spec generates in ~0.3s (you'll get a tool-explosion warning telling you to curate with `--include`, because a 1,194-tool MCP server is how you get [the Whisper problem](#why-now)).
170
+
171
+ ## Status & roadmap
172
+
173
+ v0.1 — working generator, signup server, JWT attestation verification, scope-enforcing middleware, per-key/per-IP rate limits, agent-readiness scoring. 12 passing tests (`npm test`). Roadmap:
174
+
175
+ - [x] Hosted `/mcp` endpoint — one origin serves discovery, signup, and tools
176
+ - [x] Mountable gateway — run everything inside your existing app, one process
177
+ - [x] Stripe metered billing per key (agents as paying customers)
178
+ - [x] Key revocation API
179
+ - [x] CI gate: `keymaker score --json --min 80`
180
+ - [ ] Managed hosting (`yourapi.keymaker.dev`) — no infra to run
181
+ - [ ] x402 fallback for account-less micropayment access
182
+ - [ ] Registry submission: publish generated MCP servers to agent tool registries
183
+
184
+ MIT. Built with the [MCP SDK](https://github.com/modelcontextprotocol/typescript-sdk).
@@ -0,0 +1,69 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: Todozilla API
4
+ version: 1.0.0
5
+ description: A simple task-management API used to demo keymaker.
6
+ servers:
7
+ - url: https://api.todozilla.example.com/v1
8
+ components:
9
+ securitySchemes:
10
+ bearerAuth:
11
+ type: http
12
+ scheme: bearer
13
+ description: API key issued via /agent-auth (see auth.md)
14
+ paths:
15
+ /tasks:
16
+ get:
17
+ operationId: listTasks
18
+ tags: [Tasks]
19
+ summary: List tasks
20
+ description: Returns tasks, newest first. Supports filtering by status.
21
+ parameters:
22
+ - name: status
23
+ in: query
24
+ description: Filter by task status
25
+ schema: { type: string, enum: [open, done] }
26
+ - name: limit
27
+ in: query
28
+ description: Max results (default 20)
29
+ schema: { type: integer }
30
+ responses:
31
+ "200": { description: OK }
32
+ post:
33
+ operationId: createTask
34
+ tags: [Tasks]
35
+ summary: Create a task
36
+ description: Creates a new task and returns it.
37
+ requestBody:
38
+ content:
39
+ application/json:
40
+ schema:
41
+ type: object
42
+ required: [title]
43
+ properties:
44
+ title: { type: string, description: Task title }
45
+ due: { type: string, format: date, description: Due date (YYYY-MM-DD) }
46
+ responses:
47
+ "201": { description: Created }
48
+ /tasks/{taskId}:
49
+ get:
50
+ operationId: getTask
51
+ tags: [Tasks]
52
+ summary: Get one task
53
+ parameters:
54
+ - name: taskId
55
+ in: path
56
+ required: true
57
+ schema: { type: string }
58
+ responses:
59
+ "200": { description: OK }
60
+ delete:
61
+ operationId: deleteTask
62
+ tags: [Tasks]
63
+ parameters:
64
+ - name: taskId
65
+ in: path
66
+ required: true
67
+ schema: { type: string }
68
+ responses:
69
+ "204": { description: Deleted }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "keymaker-cli",
3
+ "version": "0.1.0",
4
+ "description": "Make your API agent-ready in an afternoon: OpenAPI spec → curated MCP server + llms.txt + auth.md agent signup",
5
+ "type": "module",
6
+ "bin": {
7
+ "keymaker-cli": "./src/cli.js",
8
+ "keymaker": "./src/cli.js",
9
+ "agent-ready": "./src/cli.js"
10
+ },
11
+ "main": "src/index.js",
12
+ "exports": {
13
+ ".": "./src/index.js"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "examples/todo-api.yaml",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "test": "node --test test/*.test.mjs",
23
+ "demo": "node src/cli.js generate examples/todo-api.yaml -o out/todo && node src/cli.js serve out/todo"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/adityaaa-IIT-BHU/keymaker.git"
28
+ },
29
+ "homepage": "https://github.com/adityaaa-IIT-BHU/keymaker#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/adityaaa-IIT-BHU/keymaker/issues"
32
+ },
33
+ "keywords": [
34
+ "mcp",
35
+ "model-context-protocol",
36
+ "openapi",
37
+ "ai-agents",
38
+ "llms-txt",
39
+ "auth-md",
40
+ "agent-auth",
41
+ "api"
42
+ ],
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "license": "MIT",
47
+ "author": "Aditya Acharya <aditya.acharya.bme23@itbhu.ac.in>",
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.0.0",
50
+ "commander": "^12.0.0",
51
+ "js-yaml": "^4.1.0"
52
+ }
53
+ }
package/src/attest.js ADDED
@@ -0,0 +1,73 @@
1
+ import { createPublicKey, verify as cryptoVerify } from "node:crypto";
2
+
3
+ function b64urlJson(s) {
4
+ return JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
5
+ }
6
+
7
+ /**
8
+ * Verify an agent-platform attestation (ID-JAG-style JWT).
9
+ * config.trusted_issuers: [{ issuer, jwks_url }] or [{ issuer, jwks: [<JWK>] }]
10
+ * config.audience: optional expected aud
11
+ * config.dev_accept_any_attestation: accept any non-empty token (local testing only)
12
+ */
13
+ export async function verifyAttestation(token, config = {}) {
14
+ const issuers = config.trusted_issuers ?? [];
15
+ if (!issuers.length) {
16
+ return config.dev_accept_any_attestation
17
+ ? { verified: true, mode: "dev-accepted", claims: null }
18
+ : { verified: false, reason: "no trusted_issuers configured (see signup.config.json)" };
19
+ }
20
+
21
+ let header, payload, signature, signingInput;
22
+ try {
23
+ const parts = String(token).split(".");
24
+ if (parts.length !== 3) throw new Error("not a JWT");
25
+ header = b64urlJson(parts[0]);
26
+ payload = b64urlJson(parts[1]);
27
+ signature = Buffer.from(parts[2], "base64url");
28
+ signingInput = Buffer.from(`${parts[0]}.${parts[1]}`);
29
+ } catch {
30
+ return { verified: false, reason: "malformed attestation token" };
31
+ }
32
+
33
+ const issuer = issuers.find((i) => i.issuer === payload.iss);
34
+ if (!issuer) return { verified: false, reason: `untrusted issuer: ${payload.iss}` };
35
+ if (payload.exp && payload.exp * 1000 < Date.now()) {
36
+ return { verified: false, reason: "attestation expired" };
37
+ }
38
+ if (config.audience) {
39
+ const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
40
+ if (!aud.includes(config.audience)) return { verified: false, reason: "audience mismatch" };
41
+ }
42
+
43
+ let jwks = issuer.jwks;
44
+ if (!jwks && issuer.jwks_url) {
45
+ try {
46
+ const res = await fetch(issuer.jwks_url);
47
+ if (!res.ok) return { verified: false, reason: `JWKS fetch failed: HTTP ${res.status}` };
48
+ jwks = (await res.json()).keys;
49
+ } catch (err) {
50
+ return { verified: false, reason: `JWKS fetch failed: ${err.message}` };
51
+ }
52
+ }
53
+ const jwk = (jwks ?? []).find((k) => !header.kid || k.kid === header.kid);
54
+ if (!jwk) return { verified: false, reason: "no matching key in JWKS" };
55
+
56
+ let key;
57
+ try {
58
+ key = createPublicKey({ key: jwk, format: "jwk" });
59
+ } catch {
60
+ return { verified: false, reason: "unusable JWK" };
61
+ }
62
+
63
+ let ok = false;
64
+ if (header.alg === "RS256") ok = cryptoVerify("RSA-SHA256", signingInput, key, signature);
65
+ else if (header.alg === "ES256")
66
+ ok = cryptoVerify("SHA256", signingInput, { key, dsaEncoding: "ieee-p1363" }, signature);
67
+ else if (header.alg === "EdDSA") ok = cryptoVerify(null, signingInput, key, signature);
68
+ else return { verified: false, reason: `unsupported alg: ${header.alg}` };
69
+
70
+ return ok
71
+ ? { verified: true, mode: "jwt-verified", claims: payload }
72
+ : { verified: false, reason: "bad signature" };
73
+ }
package/src/billing.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Config-gated billing provider. Enable in signup.config.json:
3
+ * "billing": {
4
+ * "provider": "stripe",
5
+ * "secret_key_env": "STRIPE_SECRET_KEY",
6
+ * "meter_event_name": "keymaker_api_call"
7
+ * }
8
+ * On registration the agent becomes a Stripe customer; every metered request
9
+ * emits a Billing Meter event, so a metered price turns usage into invoices.
10
+ * Returns null when unconfigured — billing is always optional.
11
+ */
12
+ export function createBilling(cfg) {
13
+ if (!cfg || cfg.provider !== "stripe") return null;
14
+ const key = process.env[cfg.secret_key_env ?? "STRIPE_SECRET_KEY"];
15
+ if (!key) return null;
16
+ const base = (cfg.api_base ?? "https://api.stripe.com").replace(/\/$/, "");
17
+ const eventName = cfg.meter_event_name ?? "keymaker_api_call";
18
+
19
+ const call = async (path, params) => {
20
+ const res = await fetch(`${base}${path}`, {
21
+ method: "POST",
22
+ headers: {
23
+ authorization: `Bearer ${key}`,
24
+ "content-type": "application/x-www-form-urlencoded",
25
+ },
26
+ body: new URLSearchParams(params),
27
+ });
28
+ if (!res.ok) throw new Error(`Stripe ${path}: HTTP ${res.status} ${await res.text()}`);
29
+ return res.json();
30
+ };
31
+
32
+ return {
33
+ async onRegister(record, body = {}) {
34
+ const params = { name: record.client_name, "metadata[key_id]": record.key_id };
35
+ if (body.billing_email) params.email = String(body.billing_email).slice(0, 200);
36
+ const customer = await call("/v1/customers", params);
37
+ return { customer_id: customer.id };
38
+ },
39
+ async onMeter(record) {
40
+ if (!record.billing_customer_id) return;
41
+ await call("/v1/billing/meter_events", {
42
+ event_name: eventName,
43
+ "payload[stripe_customer_id]": record.billing_customer_id,
44
+ "payload[value]": "1",
45
+ });
46
+ },
47
+ };
48
+ }
package/src/cli.js ADDED
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { mkdir, writeFile, access } from "node:fs/promises";
4
+ import { randomBytes } from "node:crypto";
5
+ import { join } from "node:path";
6
+ import { loadSpec } from "./parse.js";
7
+ import { extractOperations } from "./extract.js";
8
+ import { renderMcpServer, toTools } from "./gen-mcp.js";
9
+ import { renderLlmsTxt } from "./gen-llms.js";
10
+ import { renderAuthMd } from "./gen-authmd.js";
11
+ import { startSignupServer } from "./serve.js";
12
+ import { scoreSpec } from "./score.js";
13
+
14
+ const program = new Command();
15
+ program
16
+ .name("keymaker")
17
+ .description("Make your API agent-ready: OpenAPI → curated MCP server + llms.txt + auth.md agent signup")
18
+ .version("0.1.0");
19
+
20
+ program
21
+ .command("generate")
22
+ .argument("<spec>", "OpenAPI spec path or URL (JSON or YAML)")
23
+ .option("-o, --out <dir>", "output directory", "agent-ready")
24
+ .option("--base-url <url>", "override API base URL")
25
+ .option("--service-url <url>", "public URL where the signup server runs", "http://localhost:8787")
26
+ .option("--include <names...>", "only operations whose name contains one of these")
27
+ .option("--exclude <names...>", "skip operations whose name contains one of these")
28
+ .action(async (specSrc, opts) => {
29
+ const spec = await loadSpec(specSrc);
30
+ const baseUrl = (opts.baseUrl ?? spec.servers?.[0]?.url ?? "http://localhost:3000").replace(/\/$/, "");
31
+ const { ops, warnings } = extractOperations(spec, { include: opts.include, exclude: opts.exclude });
32
+ if (!ops.length) {
33
+ console.error("No operations found in spec (check --include/--exclude filters).");
34
+ process.exit(1);
35
+ }
36
+ const title = spec.info?.title ?? "api";
37
+ await mkdir(join(opts.out, ".well-known"), { recursive: true });
38
+ const authMd = renderAuthMd({ spec, serviceUrl: opts.serviceUrl });
39
+ await writeFile(
40
+ join(opts.out, "mcp-server.mjs"),
41
+ renderMcpServer({ title, version: spec.info?.version ?? "0.0.0", baseUrl, ops })
42
+ );
43
+ await writeFile(join(opts.out, "llms.txt"), renderLlmsTxt({ spec, ops, baseUrl, serviceUrl: opts.serviceUrl }));
44
+ await writeFile(join(opts.out, "auth.md"), authMd);
45
+ await writeFile(join(opts.out, ".well-known", "auth.md"), authMd);
46
+ await writeFile(
47
+ join(opts.out, "tools.json"),
48
+ JSON.stringify(
49
+ { title, version: spec.info?.version ?? "0.0.0", baseUrl, tools: toTools(ops) },
50
+ null,
51
+ 2
52
+ )
53
+ );
54
+
55
+ const configPath = join(opts.out, "signup.config.json");
56
+ try {
57
+ await access(configPath);
58
+ } catch {
59
+ await writeFile(
60
+ configPath,
61
+ JSON.stringify(
62
+ {
63
+ _note:
64
+ "dev_accept_any_attestation accepts ANY attestation string — local testing only. For production, set trusted_issuers: [{ issuer, jwks_url }] and remove the dev flag.",
65
+ dev_accept_any_attestation: true,
66
+ audience: null,
67
+ trusted_issuers: [],
68
+ registrations_per_minute_per_ip: 10,
69
+ verifies_per_minute_per_key: 120,
70
+ admin_token: `adm_${randomBytes(16).toString("hex")}`,
71
+ },
72
+ null,
73
+ 2
74
+ )
75
+ );
76
+ }
77
+
78
+ console.log(`✔ ${title}: ${ops.length} operations → ${opts.out}/`);
79
+ console.log(" mcp-server.mjs llms.txt auth.md .well-known/auth.md tools.json signup.config.json");
80
+ if (warnings.length) {
81
+ console.log("\nCuration report — agents choose tools by docs quality; fix these in your spec:");
82
+ for (const w of warnings.slice(0, 20)) console.log(` ⚠ ${w}`);
83
+ if (warnings.length > 20) console.log(` …and ${warnings.length - 20} more`);
84
+ }
85
+ if (ops.length > 40) {
86
+ console.log(
87
+ `\n⚠ ${ops.length} tools is tool-explosion territory — agents pick tools better from small, curated sets.` +
88
+ `\n Narrow it: keymaker generate <spec> --include <names…> (or --exclude)`
89
+ );
90
+ }
91
+ const { total, grade } = scoreSpec(spec, ops, warnings);
92
+ console.log(`\nAgent-readiness score: ${total}/100 (${grade}) — run \`keymaker score\` for the breakdown`);
93
+ console.log(`\nNext:`);
94
+ console.log(` keymaker serve ${opts.out} # agent-signup endpoint`);
95
+ console.log(` node ${join(opts.out, "mcp-server.mjs")} # MCP server (stdio)`);
96
+ });
97
+
98
+ program
99
+ .command("score")
100
+ .argument("<spec>", "OpenAPI spec path or URL")
101
+ .description("Grade how agent-ready an API spec is (like Lighthouse, for agents)")
102
+ .option("--json", "machine-readable output")
103
+ .option("--min <score>", "exit non-zero if the score is below this (for CI)")
104
+ .action(async (specSrc, opts) => {
105
+ const spec = await loadSpec(specSrc);
106
+ const { ops, warnings } = extractOperations(spec);
107
+ const { total, grade, parts } = scoreSpec(spec, ops, warnings);
108
+ if (opts.json) {
109
+ console.log(JSON.stringify({ title: spec.info?.title ?? "API", operations: ops.length, total, grade, parts }, null, 2));
110
+ } else {
111
+ console.log(`Agent-readiness: ${total}/100 grade ${grade} (${spec.info?.title ?? "API"}, ${ops.length} operations)\n`);
112
+ for (const p of parts) {
113
+ const bar = "█".repeat(Math.round((10 * p.got) / p.max)).padEnd(10, "░");
114
+ console.log(` ${bar} ${String(p.got).padStart(3)}/${p.max} ${p.name}${p.fix ? ` → ${p.fix}` : ""}`);
115
+ }
116
+ console.log(`\nAgents choose tools by documentation quality. Fix the arrows, re-run, ship.`);
117
+ }
118
+ if (opts.min && total < Number(opts.min)) {
119
+ console.error(`\n✖ score ${total} is below --min ${opts.min}`);
120
+ process.exit(1);
121
+ }
122
+ });
123
+
124
+ program
125
+ .command("doctor")
126
+ .argument("<hosts...>", "domain(s) to check, e.g. api.example.com docs.example.com")
127
+ .description("Check a LIVE site for agent surfaces: llms.txt, auth.md, /mcp")
128
+ .option("--json", "machine-readable output")
129
+ .action(async (hosts, opts) => {
130
+ const { checkSite, formatDoctor } = await import("./doctor.js");
131
+ const results = [];
132
+ for (const host of hosts) {
133
+ const r = await checkSite(host);
134
+ results.push(r);
135
+ if (!opts.json) console.log(formatDoctor(r));
136
+ }
137
+ if (opts.json) console.log(JSON.stringify(results, null, 2));
138
+ const invisible = results.filter((r) => !r.llms_txt && !r.auth_md && !r.mcp).length;
139
+ if (!opts.json && results.length > 1) {
140
+ console.log(`\n${invisible}/${results.length} hosts have zero agent surfaces. keymaker generate fixes all three.`);
141
+ }
142
+ });
143
+
144
+ program
145
+ .command("serve")
146
+ .argument("[dir]", "generated directory", "agent-ready")
147
+ .option("-p, --port <port>", "port", "8787")
148
+ .action(async (dir, opts) => {
149
+ const server = await startSignupServer({ dir, port: Number(opts.port) });
150
+ const port = server.address().port;
151
+ console.log(`Agent signup live on http://localhost:${port}`);
152
+ console.log(` GET /auth.md registration instructions (also /.well-known/auth.md, /llms.txt)`);
153
+ console.log(` POST /agent-auth agent registers, gets a scoped key (temp until claimed, or attested)`);
154
+ console.log(` POST /agent-auth/claim human claims a temporary key`);
155
+ console.log(` POST /agent-auth/verify your backend verifies keys + meters usage`);
156
+ console.log(` GET /agent-auth/keys issued keys (redacted)`);
157
+ });
158
+
159
+ program.parse();
package/src/doctor.js ADDED
@@ -0,0 +1,63 @@
1
+ const UA = "keymaker-doctor/0.1 (+https://github.com/adityaaa-IIT-BHU/keymaker)";
2
+
3
+ async function probe(url, init = {}) {
4
+ const controller = new AbortController();
5
+ const timer = setTimeout(() => controller.abort(), 8000);
6
+ try {
7
+ const res = await fetch(url, {
8
+ ...init,
9
+ redirect: "follow",
10
+ signal: controller.signal,
11
+ headers: { "user-agent": UA, ...(init.headers ?? {}) },
12
+ });
13
+ const text = await res.text().catch(() => "");
14
+ return { status: res.status, type: res.headers.get("content-type") ?? "", body: text.slice(0, 2000) };
15
+ } catch {
16
+ return { status: 0, type: "", body: "" };
17
+ } finally {
18
+ clearTimeout(timer);
19
+ }
20
+ }
21
+
22
+ function looksLikeMarkdownDoc(r) {
23
+ if (r.status !== 200) return false;
24
+ const body = r.body.trimStart();
25
+ // SPA catch-alls return 200 HTML for any path — that's a miss, not a hit
26
+ if (body.startsWith("<") || r.type.includes("text/html")) return false;
27
+ return body.startsWith("#") || /\[[^\]]+\]\([^)]+\)/.test(body);
28
+ }
29
+
30
+ function looksLikeMcp(r) {
31
+ if (r.status === 0 || r.status === 404) return false;
32
+ if (r.type.includes("text/html")) return false;
33
+ // A real MCP endpoint answers POSTs with JSON-RPC (or auth/method errors), not a marketing page
34
+ return [400, 401, 405, 406].includes(r.status) || r.body.includes("jsonrpc") || r.type.includes("json") || r.type.includes("event-stream");
35
+ }
36
+
37
+ /** Check a live host for the three agent surfaces. Returns presence + evidence. */
38
+ export async function checkSite(base) {
39
+ const origin = base.replace(/\/$/, "").replace(/^(?!https?:\/\/)/, "https://");
40
+ const [llms, llmsFull, authmd, wellKnown, mcp] = await Promise.all([
41
+ probe(`${origin}/llms.txt`),
42
+ probe(`${origin}/llms-full.txt`),
43
+ probe(`${origin}/auth.md`),
44
+ probe(`${origin}/.well-known/auth.md`),
45
+ probe(`${origin}/mcp`, {
46
+ method: "POST",
47
+ headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
48
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "keymaker-doctor", version: "0.1" } } }),
49
+ }),
50
+ ]);
51
+ return {
52
+ host: origin.replace(/^https?:\/\//, ""),
53
+ llms_txt: looksLikeMarkdownDoc(llms) || looksLikeMarkdownDoc(llmsFull),
54
+ auth_md: looksLikeMarkdownDoc(authmd) || looksLikeMarkdownDoc(wellKnown),
55
+ mcp: looksLikeMcp(mcp),
56
+ };
57
+ }
58
+
59
+ export function formatDoctor(result) {
60
+ const mark = (b) => (b ? "✓" : "✗");
61
+ const count = [result.llms_txt, result.auth_md, result.mcp].filter(Boolean).length;
62
+ return `${result.host}: ${count}/3 agent surfaces llms.txt ${mark(result.llms_txt)} auth.md ${mark(result.auth_md)} /mcp ${mark(result.mcp)}`;
63
+ }