@reclaimprotocol/agent 0.1.11 → 0.1.12

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.
@@ -71,6 +71,20 @@ export interface MyProvidersQuery {
71
71
  pageSize?: number;
72
72
  searchQuery?: string;
73
73
  }
74
+ /** `POST /api/applications/issue-credentials` response — a brand-new,
75
+ * unassociated (sandboxed) app. `appSecret` is the raw eth private key;
76
+ * it's returned once and never stored server-side in recoverable form. */
77
+ export interface IssuedAppCredentials {
78
+ appId: string;
79
+ appSecret: string;
80
+ }
81
+ /** `GET /api/applications/link/nonce` response — a one-time, 5-minute-TTL
82
+ * challenge the caller signs with the app's private key to prove ownership
83
+ * in `linkAppToAccount`. */
84
+ export interface LinkNonce {
85
+ nonce: string;
86
+ expiresInSeconds: number;
87
+ }
74
88
  /**
75
89
  * Minimal REST client for the OLD devtools Express backend (base `/api`).
76
90
  * Deliberately NOT a clone of `@reclaimprotocol/client`'s operationId
@@ -110,5 +124,22 @@ export declare class ReclaimOldClient {
110
124
  * `#request`). Attaches the caller identity when one is set; queried
111
125
  * anonymously otherwise. */
112
126
  getSessionAnalyticsLogs(sessionId: string): Promise<unknown>;
127
+ /** Mint a brand-new, unassociated (sandboxed) app — no auth required. Link
128
+ * it to an account afterwards with `getLinkNonce` + `linkAppToAccount`, or
129
+ * use it as-is within the sandbox session limit. */
130
+ issueCredentials(): Promise<IssuedAppCredentials>;
131
+ /** One-time, 5-minute challenge for `linkAppToAccount`. Requires auth —
132
+ * the nonce is scoped to the authenticated user's uid. */
133
+ getLinkNonce(): Promise<LinkNonce>;
134
+ /** Link an unassociated (`issueCredentials`-minted) app to the
135
+ * authenticated account. `signature` must be an EIP-191 signature, made
136
+ * with the app's private key, over the exact string
137
+ * `` `Link ${appId} to account ${userId} | nonce: ${nonce}` `` — see
138
+ * `buildLinkMessage` in the backend's `application.controller.ts`. Fails
139
+ * with `APP_ALREADY_LINKED` (400) if the app already belongs to someone. */
140
+ linkAppToAccount(appId: string, signature: string, nonce: string): Promise<unknown>;
141
+ /** Public, unauthenticated app status — `isLinked`, sandbox/quota info.
142
+ * Works for any appId, owned or not. */
143
+ getApplicationStatus(appId: string): Promise<unknown>;
113
144
  getMyProviders(query?: MyProvidersQuery): Promise<unknown>;
114
145
  }
package/lib/old/client.js CHANGED
@@ -89,6 +89,39 @@ export class ReclaimOldClient {
89
89
  }
90
90
  return json;
91
91
  }
92
+ /** Mint a brand-new, unassociated (sandboxed) app — no auth required. Link
93
+ * it to an account afterwards with `getLinkNonce` + `linkAppToAccount`, or
94
+ * use it as-is within the sandbox session limit. */
95
+ async issueCredentials() {
96
+ const res = await __classPrivateFieldGet(this, _ReclaimOldClient_instances, "m", _ReclaimOldClient_request).call(this, 'POST', '/applications/issue-credentials');
97
+ return res.data;
98
+ }
99
+ /** One-time, 5-minute challenge for `linkAppToAccount`. Requires auth —
100
+ * the nonce is scoped to the authenticated user's uid. */
101
+ async getLinkNonce() {
102
+ const res = await __classPrivateFieldGet(this, _ReclaimOldClient_instances, "m", _ReclaimOldClient_request).call(this, 'GET', '/applications/link/nonce', {
103
+ auth: true,
104
+ });
105
+ return res.data;
106
+ }
107
+ /** Link an unassociated (`issueCredentials`-minted) app to the
108
+ * authenticated account. `signature` must be an EIP-191 signature, made
109
+ * with the app's private key, over the exact string
110
+ * `` `Link ${appId} to account ${userId} | nonce: ${nonce}` `` — see
111
+ * `buildLinkMessage` in the backend's `application.controller.ts`. Fails
112
+ * with `APP_ALREADY_LINKED` (400) if the app already belongs to someone. */
113
+ linkAppToAccount(appId, signature, nonce) {
114
+ return __classPrivateFieldGet(this, _ReclaimOldClient_instances, "m", _ReclaimOldClient_request).call(this, 'POST', '/applications/link', {
115
+ body: { appId, signature, nonce },
116
+ auth: true,
117
+ });
118
+ }
119
+ /** Public, unauthenticated app status — `isLinked`, sandbox/quota info.
120
+ * Works for any appId, owned or not. */
121
+ async getApplicationStatus(appId) {
122
+ const res = await __classPrivateFieldGet(this, _ReclaimOldClient_instances, "m", _ReclaimOldClient_request).call(this, 'GET', `/applications/status/${encodeURIComponent(appId)}`);
123
+ return res.data;
124
+ }
92
125
  getMyProviders(query = {}) {
93
126
  // Every one of these query params is REQUIRED by the backend
94
127
  // validator (missing/invalid → 400), so default them all here.
@@ -1,9 +1,15 @@
1
1
  import { authenticateTool } from "./authenticate.js";
2
+ import { checkAppStatusTool } from "./check-app-status.js";
2
3
  import { getProviderInfoTool } from "./get-provider-info.js";
4
+ import { issueAppCredentialsTool } from "./issue-app-credentials.js";
5
+ import { getJsSdkIntegrationGuideTool } from "./js-sdk-integration-guide.js";
6
+ import { linkAppTool } from "./link-app.js";
3
7
  import { getMeProvidersTool } from "./me-providers.js";
4
8
  import { createVersionFromCaptureTool } from "./publish.js";
9
+ import { scaffoldDemoServerTool } from "./scaffold-demo-server.js";
5
10
  import { sessionAnalyticsLogsTool } from "./session-analytics-logs.js";
6
11
  import { updateProviderMetadataTool } from "./update-provider-metadata.js";
12
+ import { verifySdkSetupTool } from "./verify-sdk-setup.js";
7
13
  /**
8
14
  * The backend-facing tools for OLD-devtools mode. Mirrors
9
15
  * `buildCredentialTools` / `buildAgentTools`: returns a flat `RegisteredTool`
@@ -22,5 +28,11 @@ export function buildOldBackendTools(client, sdkClient) {
22
28
  getProviderInfoTool(sdkClient),
23
29
  updateProviderMetadataTool(client),
24
30
  sessionAnalyticsLogsTool(client),
31
+ issueAppCredentialsTool(client),
32
+ linkAppTool(client),
33
+ checkAppStatusTool(client),
34
+ getJsSdkIntegrationGuideTool(),
35
+ scaffoldDemoServerTool(),
36
+ verifySdkSetupTool(client),
25
37
  ];
26
38
  }
@@ -0,0 +1,8 @@
1
+ import { type RegisteredTool } from '../../mcp/server.ts';
2
+ import type { ReclaimOldClient } from '../client.ts';
3
+ /**
4
+ * Public, unauthenticated app status lookup — works for any `appId`, owned
5
+ * or not, linked or sandboxed. Mirrors the backend's
6
+ * `GET /api/applications/status/:appId`.
7
+ */
8
+ export declare function checkAppStatusTool(client: ReclaimOldClient): RegisteredTool;
@@ -0,0 +1,27 @@
1
+ import { defineTool } from "../../mcp/server.js";
2
+ /**
3
+ * Public, unauthenticated app status lookup — works for any `appId`, owned
4
+ * or not, linked or sandboxed. Mirrors the backend's
5
+ * `GET /api/applications/status/:appId`.
6
+ */
7
+ export function checkAppStatusTool(client) {
8
+ return defineTool({
9
+ name: 'check_app_status',
10
+ description: 'Check a Reclaim app\'s link/sandbox status by appId — no ' +
11
+ 'authentication required. Returns whether the app is linked to a ' +
12
+ 'dashboard account (`isLinked`), and for linked apps, remaining ' +
13
+ 'session quota. A 404 means the appId has never been used yet — ' +
14
+ 'it is created lazily on the first ReclaimProofRequest.init() call ' +
15
+ 'or via issue_app_credentials.',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ appId: {
20
+ type: 'string',
21
+ description: 'The eth-address appId to check.',
22
+ },
23
+ },
24
+ required: ['appId'],
25
+ },
26
+ }, async ({ appId }) => client.getApplicationStatus(appId));
27
+ }
@@ -0,0 +1,11 @@
1
+ import { type RegisteredTool } from '../../mcp/server.ts';
2
+ import type { ReclaimOldClient } from '../client.ts';
3
+ /**
4
+ * Mint a brand-new eth-keypair APP (as opposed to authenticating as a
5
+ * dashboard user). `appId` is the eth address, `appSecret` the raw private
6
+ * key — this is the pair `@reclaimprotocol/js-sdk`'s `ReclaimProofRequest.init`
7
+ * expects. No auth required: the app starts unassociated (`creatorUid: null`)
8
+ * and sandboxed (`sandboxLimit: 100` sessions). Use `link_app_to_account` to
9
+ * attach it to your dashboard account, or `check_app_status` to inspect it.
10
+ */
11
+ export declare function issueAppCredentialsTool(client: ReclaimOldClient): RegisteredTool;
@@ -0,0 +1,35 @@
1
+ import { defineTool } from "../../mcp/server.js";
2
+ /**
3
+ * Mint a brand-new eth-keypair APP (as opposed to authenticating as a
4
+ * dashboard user). `appId` is the eth address, `appSecret` the raw private
5
+ * key — this is the pair `@reclaimprotocol/js-sdk`'s `ReclaimProofRequest.init`
6
+ * expects. No auth required: the app starts unassociated (`creatorUid: null`)
7
+ * and sandboxed (`sandboxLimit: 100` sessions). Use `link_app_to_account` to
8
+ * attach it to your dashboard account, or `check_app_status` to inspect it.
9
+ */
10
+ export function issueAppCredentialsTool(client) {
11
+ return defineTool({
12
+ name: 'issue_app_credentials',
13
+ description: 'Mint a new Reclaim APP (eth keypair) on the old devtools backend — ' +
14
+ 'no authentication required. Returns `{ appId, appSecret }`: ' +
15
+ '`appId` is the eth address, `appSecret` the raw private key. This ' +
16
+ 'is the appId/appSecret pair for @reclaimprotocol/js-sdk ' +
17
+ '(`ReclaimProofRequest.init(appId, appSecret, providerId)`). The app ' +
18
+ 'starts UNLINKED and sandboxed (100 sessions) — call ' +
19
+ 'link_app_to_account to attach it to your dashboard account and ' +
20
+ 'check_app_status to check its state. Store `appSecret` like a ' +
21
+ 'password (e.g. in a gitignored .env as RECLAIM_APP_SECRET) — it ' +
22
+ 'is returned once and cannot be recovered from the server.',
23
+ inputSchema: { type: 'object', properties: {} },
24
+ }, async () => {
25
+ const { appId, appSecret } = await client.issueCredentials();
26
+ return {
27
+ appId,
28
+ appSecret,
29
+ note: 'Unlinked, sandboxed app (100 session limit). Set RECLAIM_APP_ID ' +
30
+ 'and RECLAIM_APP_SECRET in your .env (gitignored). Run ' +
31
+ 'link_app_to_account to attach it to your dashboard account and ' +
32
+ 'lift the sandbox limit.',
33
+ };
34
+ });
35
+ }
@@ -0,0 +1,2 @@
1
+ import { type RegisteredTool } from '../../mcp/server.ts';
2
+ export declare function getJsSdkIntegrationGuideTool(): RegisteredTool;
@@ -0,0 +1,141 @@
1
+ import { defineTool } from "../../mcp/server.js";
2
+ /**
3
+ * Verified against `@reclaimprotocol/js-sdk` v5.8.1 source
4
+ * (`reclaim-js-sdk/src/Reclaim.ts`) — NOT copied unchecked from the archived
5
+ * `reclaim-mcp-server`'s guide, which predates several of these signatures.
6
+ * Corrections that matter most, all called out inline below:
7
+ * - `getRequestUrl()` and `ReclaimProofRequest.fromJsonString()` are ASYNC.
8
+ * - `setContext` is current; `addContext` is deprecated (calls setContext).
9
+ * - `verifyProof`'s second arg (`{ providerId, providerVersion? }`) is
10
+ * REQUIRED, not optional.
11
+ * - The verified result's flag is `isVerified` (camelCase), not
12
+ * `is_verified`.
13
+ * - A raw webhook body is TEXT — it must be JSON-parsed (and URL-decoded if
14
+ * form-encoded) into a `Proof`/`Proof[]` before calling `verifyProof`.
15
+ */
16
+ const GUIDE = `# @reclaimprotocol/js-sdk — backend integration guide
17
+
18
+ Keep \`appSecret\` on your BACKEND only. Everything below runs server-side;
19
+ the browser only ever sees the \`requestUrl\`.
20
+
21
+ ## Testing locally, without deploying anything
22
+
23
+ - **providerId \`"example"\`**: a canonical built-in smoke-test provider —
24
+ no need to author/publish your own provider just to confirm the SDK
25
+ plumbing works end-to-end. Swap in your real providerId once this passes.
26
+ - **ngrok for the callback URL**: \`setAppCallbackUrl\` needs a URL Reclaim's
27
+ servers can reach over the public internet — \`localhost\` will never
28
+ receive the callback. Run \`ngrok http 3000\` (or your port) alongside your
29
+ local server and set \`PUBLIC_URL\` to the \`https://<id>.ngrok-free.app\`
30
+ URL it prints. Without this, only the frontend-side flow (opening
31
+ \`requestUrl\` and polling/reading the result client-side) will work — the
32
+ async webhook never lands.
33
+ - \`scaffold_reclaim_demo_server\` defaults \`providerId\` to \`"example"\` and
34
+ its README calls out the ngrok step — use it to get a working local loop
35
+ before wiring in a real provider.
36
+
37
+ ## 1. Install
38
+
39
+ \`\`\`bash
40
+ npm install @reclaimprotocol/js-sdk
41
+ \`\`\`
42
+
43
+ ## 2. Create a proof request (backend)
44
+
45
+ \`\`\`js
46
+ import { ReclaimProofRequest } from "@reclaimprotocol/js-sdk";
47
+
48
+ const reclaimProofRequest = await ReclaimProofRequest.init(
49
+ process.env.RECLAIM_APP_ID,
50
+ process.env.RECLAIM_APP_SECRET,
51
+ providerId,
52
+ {
53
+ // acceptAiProviders: true, // allow AI-verified providers
54
+ // useAppClip: true, // mobile app-clip flow instead of a browser popup
55
+ // providerVersion: "1.0.0", // pin a specific provider version
56
+ },
57
+ );
58
+
59
+ // Deliver the verified proof back to your server asynchronously instead of
60
+ // (or in addition to) reading it from the frontend callback:
61
+ reclaimProofRequest.setAppCallbackUrl(
62
+ \`\${process.env.PUBLIC_URL}/webhooks/reclaim\`,
63
+ true, // jsonProofResponse: send the callback body as JSON, not form-encoded
64
+ );
65
+
66
+ // Optional: attach your own correlation id / metadata, retrievable later.
67
+ reclaimProofRequest.setContext(sessionId, JSON.stringify({ createdAt: Date.now() }));
68
+
69
+ // IMPORTANT: getRequestUrl() is ASYNC — await it.
70
+ const requestUrl = await reclaimProofRequest.getRequestUrl();
71
+
72
+ // Hand the requestUrl to the frontend to open in a new tab/window/webview.
73
+ // If you need to rehydrate this request object later (e.g. in the webhook
74
+ // handler, in a different process), persist reclaimProofRequest.toJsonString()
75
+ // (sync) and rehydrate with:
76
+ // const req = await ReclaimProofRequest.fromJsonString(json); // ASYNC
77
+ \`\`\`
78
+
79
+ ## 3. Frontend: open the request URL
80
+
81
+ Fetch \`requestUrl\` from your backend, then open it (popup/new tab/QR code) —
82
+ never construct it in the browser, since that would require the appSecret
83
+ client-side. Trigger the fetch+open from a real user click (not a page-load
84
+ effect) or some browsers block the popup.
85
+
86
+ ## 4. Verify the callback (backend webhook)
87
+
88
+ \`\`\`js
89
+ import { verifyProof } from "@reclaimprotocol/js-sdk";
90
+
91
+ app.post("/webhooks/reclaim", express.json(), async (req, res) => {
92
+ // req.body is already a parsed object here because jsonProofResponse was
93
+ // true above AND express.json() parsed it. If you did NOT set
94
+ // jsonProofResponse (or aren't using express.json()), the body arrives as
95
+ // form-encoded/raw text and must be JSON.parse()'d (and decodeURIComponent'd
96
+ // if form-encoded) into a Proof / Proof[] before calling verifyProof.
97
+ const proof = req.body;
98
+
99
+ // The second argument is REQUIRED — at minimum providerId.
100
+ const result = await verifyProof(proof, {
101
+ providerId,
102
+ // providerVersion, // optional, pin if you pinned it in init()
103
+ });
104
+
105
+ if (!result.isVerified) { // NOTE: isVerified, not is_verified
106
+ console.error("Proof verification failed:", result.error);
107
+ return res.status(400).json({ verified: false, error: result.error });
108
+ }
109
+
110
+ console.log("Verified data:", result.data, result.publicData);
111
+ res.status(200).json({ verified: true });
112
+ });
113
+ \`\`\`
114
+
115
+ ## Gotchas
116
+
117
+ - \`getRequestUrl()\` and \`ReclaimProofRequest.fromJsonString()\` are async —
118
+ a missing \`await\` silently hands you a Promise instead of a string/instance.
119
+ - \`verifyProof\`'s config arg is mandatory; omitting \`providerId\` throws.
120
+ - Prefer \`setContext\` over the deprecated \`addContext\` (same signature).
121
+ - \`ReclaimProofRequest.init\` needs network access to the Reclaim backend at
122
+ call time — don't call it at module load / cold-start if your platform pools
123
+ processes long-term; call it per-request instead.
124
+ - Use \`check_app_status\` (this MCP server) to confirm your app is linked
125
+ and not still sandbox-limited before debugging "verification never lands"
126
+ issues.
127
+ `;
128
+ export function getJsSdkIntegrationGuideTool() {
129
+ return defineTool({
130
+ name: 'get_js_sdk_integration_guide',
131
+ description: 'Return a verified, copy-pasteable guide for integrating ' +
132
+ '@reclaimprotocol/js-sdk (backend proof request + callback ' +
133
+ 'verification), checked against the current SDK source rather than ' +
134
+ 'assumed from older docs. Covers testing locally without deploying: ' +
135
+ 'the built-in "example" providerId for a quick smoke test, and using ' +
136
+ 'ngrok to get a callback URL Reclaim\'s servers can actually reach. ' +
137
+ 'Use scaffold_reclaim_demo_server to generate a runnable local demo ' +
138
+ 'instead of copy-pasting by hand.',
139
+ inputSchema: { type: 'object', properties: {} },
140
+ }, async () => ({ guide: GUIDE }));
141
+ }
@@ -0,0 +1,10 @@
1
+ import { type RegisteredTool } from '../../mcp/server.ts';
2
+ import type { ReclaimOldClient } from '../client.ts';
3
+ /**
4
+ * Link an unassociated app (minted via `issue_app_credentials`, or any
5
+ * eth-keypair app you already hold the private key for) to the currently
6
+ * authenticated dashboard account. Proves ownership by signing a one-time
7
+ * challenge with the app's private key — the private key itself is only
8
+ * ever used locally to sign, never sent to the server.
9
+ */
10
+ export declare function linkAppTool(client: ReclaimOldClient): RegisteredTool;
@@ -0,0 +1,72 @@
1
+ import { Wallet } from 'ethers';
2
+ import { defineTool } from "../../mcp/server.js";
3
+ /** Byte-for-byte the same format as the backend's `buildLinkMessage` in
4
+ * `application.controller.ts` — MUST stay in lockstep with it. */
5
+ function buildLinkMessage(appId, userId, nonce) {
6
+ return `Link ${appId} to account ${userId} | nonce: ${nonce}`;
7
+ }
8
+ /** The uid the backend binds into the signed message — `req.user.uid` there.
9
+ * For a bearer identity this comes back from `POST /users/login`; for an eth
10
+ * identity, `OldIdentity.uid` (already `eth:<address>`) IS that uid. */
11
+ async function resolveUserId(client) {
12
+ const identity = client.getIdentity();
13
+ if (!identity) {
14
+ throw new Error('Not authenticated against old devtools. Call reclaim_authenticate first.');
15
+ }
16
+ if (identity.kind === 'eth') {
17
+ return identity.uid;
18
+ }
19
+ const res = await client.loginWithToken();
20
+ const uid = res?.user?.uid;
21
+ if (!uid) {
22
+ throw new Error('Could not resolve your account uid from POST /users/login — ' +
23
+ 'unexpected response shape.');
24
+ }
25
+ return uid;
26
+ }
27
+ /**
28
+ * Link an unassociated app (minted via `issue_app_credentials`, or any
29
+ * eth-keypair app you already hold the private key for) to the currently
30
+ * authenticated dashboard account. Proves ownership by signing a one-time
31
+ * challenge with the app's private key — the private key itself is only
32
+ * ever used locally to sign, never sent to the server.
33
+ */
34
+ export function linkAppTool(client) {
35
+ return defineTool({
36
+ name: 'link_app_to_account',
37
+ description: 'Link an eth-keypair app to your authenticated dashboard account — ' +
38
+ 'use this to keep using an appId/appSecret you already have ' +
39
+ '(instead of minting a fresh one via issue_app_credentials). ' +
40
+ 'Requires reclaim_authenticate first. Fetches a one-time nonce, ' +
41
+ 'signs `Link <appId> to account <userId> | nonce: <nonce>` locally ' +
42
+ 'with `appSecret` (EIP-191, never transmitted), and submits the ' +
43
+ 'signature. Fails with APP_ALREADY_LINKED if the app is already ' +
44
+ 'linked to a (possibly different) account.',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ appId: {
49
+ type: 'string',
50
+ description: 'The eth-address appId to link.',
51
+ },
52
+ appSecret: {
53
+ type: 'string',
54
+ description: 'The app\'s raw eth private key — used only locally to sign ' +
55
+ 'the link challenge, never sent to the server.',
56
+ },
57
+ },
58
+ required: ['appId', 'appSecret'],
59
+ },
60
+ }, async ({ appId, appSecret }) => {
61
+ const userId = await resolveUserId(client);
62
+ const { nonce } = await client.getLinkNonce();
63
+ const message = buildLinkMessage(appId, userId, nonce);
64
+ const signature = await new Wallet(appSecret).signMessage(message);
65
+ await client.linkAppToAccount(appId, signature, nonce);
66
+ return {
67
+ linked: true,
68
+ appId,
69
+ note: 'App linked to your account. Run check_app_status to confirm.',
70
+ };
71
+ });
72
+ }
@@ -105,8 +105,15 @@ export function createVersionFromCaptureTool(client) {
105
105
  description: 'Multiple proven drafts → one multi-request version (each ' +
106
106
  'becomes a parallel `requestData` entry, all verified ' +
107
107
  'together). Each item has the same shape as `provider`. Pass ' +
108
- 'this OR `provider`, not both.',
109
- minItems: 1,
108
+ 'this OR `provider`, not both. May be omitted (or empty) ' +
109
+ 'together with `provider` ONLY when adding a version to an ' +
110
+ 'EXISTING provider (`providerId` set) — this publishes with ' +
111
+ 'zero `requestData` entries, for a pure customInjection/' +
112
+ 'HAWKEYE provider that relies entirely on ' +
113
+ '`allowedInjectedRequestData` for validation instead of a ' +
114
+ 'static requestData stub. Registering a brand-new provider ' +
115
+ 'always needs at least one draft (its name comes from the ' +
116
+ 'first one).',
110
117
  items: {
111
118
  type: 'object',
112
119
  properties: {
@@ -243,13 +250,26 @@ export function createVersionFromCaptureTool(client) {
243
250
  }, async (args) => {
244
251
  // Exactly one of `provider` / `providers` — normalize to an array so a
245
252
  // multi-request capture publishes every request, not just the first.
253
+ // Omitting both is allowed too, but ONLY when adding a version to an
254
+ // EXISTING provider (`providerId` set): that publishes with zero
255
+ // `requestData` entries, which only makes sense for a pure
256
+ // customInjection/HAWKEYE provider whose claims are validated entirely
257
+ // via `allowedInjectedRequestData` instead of a static requestData stub.
258
+ // Registering a brand-new provider (no `providerId`) still needs at
259
+ // least one draft — its name comes from the first one.
246
260
  const hasMany = Array.isArray(args.providers) && args.providers.length > 0;
247
261
  const hasOne = !!args.provider;
248
- if (hasMany === hasOne) {
249
- throw new Boom('Pass exactly one of `provider` (a single draft) or `providers` ' +
250
- '(an array of 1+ drafts).', { statusCode: 400 });
262
+ if (hasMany && hasOne) {
263
+ throw new Boom('Pass at most one of `provider` (a single draft) or `providers` ' +
264
+ '(an array of 1+ drafts) — not both.', { statusCode: 400 });
251
265
  }
252
- const providers = hasMany ? args.providers : [args.provider];
266
+ if (!hasMany && !hasOne && !args.providerId) {
267
+ throw new Boom('Pass at least one of `provider`/`providers` when registering a ' +
268
+ 'new provider (its name comes from the first draft). Omitting ' +
269
+ 'both is only allowed when adding a version to an EXISTING ' +
270
+ 'provider via `providerId`.', { statusCode: 400 });
271
+ }
272
+ const providers = hasMany ? args.providers : (hasOne ? [args.provider] : []);
253
273
  const opts = {
254
274
  initialUrl: args.initialUrl,
255
275
  description: args.description,
@@ -0,0 +1,2 @@
1
+ import { type RegisteredTool } from '../../mcp/server.ts';
2
+ export declare function scaffoldDemoServerTool(): RegisteredTool;