@proof-holdings/mcp-server 1.0.0 → 1.2.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 +1 -1
- package/README.md +220 -217
- package/dist/authBoundary.d.ts +193 -0
- package/dist/authBoundary.d.ts.map +1 -0
- package/dist/authBoundary.js +341 -0
- package/dist/authBoundary.js.map +1 -0
- package/dist/factory.d.ts +35 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +130 -0
- package/dist/factory.js.map +1 -0
- package/dist/http.d.ts +78 -3
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +251 -6
- package/dist/http.js.map +1 -1
- package/dist/remote.d.ts +111 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +789 -0
- package/dist/remote.js.map +1 -0
- package/dist/server.js +11 -57
- package/dist/server.js.map +1 -1
- package/dist/toolAnnotations.d.ts +27 -0
- package/dist/toolAnnotations.d.ts.map +1 -0
- package/dist/toolAnnotations.js +158 -0
- package/dist/toolAnnotations.js.map +1 -0
- package/dist/tools/{projects.d.ts → accounts.d.ts} +1 -1
- package/dist/tools/accounts.d.ts.map +1 -0
- package/dist/tools/accounts.js +70 -0
- package/dist/tools/accounts.js.map +1 -0
- package/dist/tools/api-keys.d.ts.map +1 -1
- package/dist/tools/api-keys.js +14 -5
- package/dist/tools/api-keys.js.map +1 -1
- package/dist/tools/auth-flows.d.ts +4 -0
- package/dist/tools/auth-flows.d.ts.map +1 -0
- package/dist/tools/auth-flows.js +34 -0
- package/dist/tools/auth-flows.js.map +1 -0
- package/dist/tools/auth.js +1 -1
- package/dist/tools/auth.js.map +1 -1
- package/dist/tools/authorizations.d.ts +4 -0
- package/dist/tools/authorizations.d.ts.map +1 -0
- package/dist/tools/authorizations.js +111 -0
- package/dist/tools/authorizations.js.map +1 -0
- package/dist/tools/circles.d.ts +4 -0
- package/dist/tools/circles.d.ts.map +1 -0
- package/dist/tools/circles.js +215 -0
- package/dist/tools/circles.js.map +1 -0
- package/dist/tools/confirmations.d.ts +4 -0
- package/dist/tools/confirmations.d.ts.map +1 -0
- package/dist/tools/confirmations.js +86 -0
- package/dist/tools/confirmations.js.map +1 -0
- package/dist/tools/delegation-verify-outcomes.d.ts +23 -0
- package/dist/tools/delegation-verify-outcomes.d.ts.map +1 -0
- package/dist/tools/delegation-verify-outcomes.js +51 -0
- package/dist/tools/delegation-verify-outcomes.js.map +1 -0
- package/dist/tools/delegation-verify.d.ts +24 -0
- package/dist/tools/delegation-verify.d.ts.map +1 -0
- package/dist/tools/delegation-verify.js +192 -0
- package/dist/tools/delegation-verify.js.map +1 -0
- package/dist/tools/delegations.d.ts +4 -0
- package/dist/tools/delegations.d.ts.map +1 -0
- package/dist/tools/delegations.js +84 -0
- package/dist/tools/delegations.js.map +1 -0
- package/dist/tools/domains.d.ts.map +1 -1
- package/dist/tools/domains.js +1 -2
- package/dist/tools/domains.js.map +1 -1
- package/dist/tools/hitl-keys.d.ts +4 -0
- package/dist/tools/hitl-keys.d.ts.map +1 -0
- package/dist/tools/hitl-keys.js +52 -0
- package/dist/tools/hitl-keys.js.map +1 -0
- package/dist/tools/hitl.d.ts +4 -0
- package/dist/tools/hitl.d.ts.map +1 -0
- package/dist/tools/hitl.js +151 -0
- package/dist/tools/hitl.js.map +1 -0
- package/dist/tools/phones.js +1 -1
- package/dist/tools/phones.js.map +1 -1
- package/dist/tools/profiles.d.ts.map +1 -1
- package/dist/tools/profiles.js +73 -0
- package/dist/tools/profiles.js.map +1 -1
- package/dist/tools/proof-me.d.ts +4 -0
- package/dist/tools/proof-me.d.ts.map +1 -0
- package/dist/tools/proof-me.js +36 -0
- package/dist/tools/proof-me.js.map +1 -0
- package/dist/tools/proofs.d.ts.map +1 -1
- package/dist/tools/proofs.js +9 -6
- package/dist/tools/proofs.js.map +1 -1
- package/dist/tools/render-auth-link.d.ts +3 -0
- package/dist/tools/render-auth-link.d.ts.map +1 -0
- package/dist/tools/render-auth-link.js +30 -0
- package/dist/tools/render-auth-link.js.map +1 -0
- package/dist/tools/sessions.js +5 -5
- package/dist/tools/sessions.js.map +1 -1
- package/dist/tools/settings.d.ts.map +1 -1
- package/dist/tools/settings.js +77 -2
- package/dist/tools/settings.js.map +1 -1
- package/dist/tools/twofa.d.ts.map +1 -1
- package/dist/tools/twofa.js +16 -3
- package/dist/tools/twofa.js.map +1 -1
- package/dist/tools/user-requests.d.ts.map +1 -1
- package/dist/tools/user-requests.js +1 -2
- package/dist/tools/user-requests.js.map +1 -1
- package/dist/tools/verification-requests.d.ts.map +1 -1
- package/dist/tools/verification-requests.js +40 -13
- package/dist/tools/verification-requests.js.map +1 -1
- package/dist/tools/verifications.d.ts.map +1 -1
- package/dist/tools/verifications.js +59 -12
- package/dist/tools/verifications.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +114 -5
- package/dist/types.js.map +1 -1
- package/package.json +11 -5
- package/dist/tools/projects.d.ts.map +0 -1
- package/dist/tools/projects.js +0 -159
- package/dist/tools/projects.js.map +0 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The boundary between what this server answers with no account and what needs one
|
|
3
|
+
* (`h-mcp-oauth-remote-wiring`).
|
|
4
|
+
*
|
|
5
|
+
* WHY IT LIVES HERE, and not inside a tool handler: the answer to "you need to sign in" is an HTTP
|
|
6
|
+
* 401 carrying `WWW-Authenticate`, and that is what a client's OAuth stack reads. By the time a tool
|
|
7
|
+
* handler runs, the response is already a JSON-RPC result — the same refusal delivered there is
|
|
8
|
+
* text, not a challenge, and every measured client ignores it (`docs/mcp-oauth-client-probe.md`).
|
|
9
|
+
* So the remote server has to classify the message BEFORE handing it to the transport, and this
|
|
10
|
+
* module is the one place that classification is written down.
|
|
11
|
+
*
|
|
12
|
+
* WHERE IT SITS, and why not on `initialize`: the control measurement in the probe put a 401 on the
|
|
13
|
+
* opening handshake and the human saw `Failed to connect — connection timed out after 30000ms`,
|
|
14
|
+
* i.e. a broken connection rather than an invitation to sign in. Anonymous connection is a closed
|
|
15
|
+
* criterion of `h-mcp-remote`, so the 401 lands on the TOOL CALL that needs an account and nowhere
|
|
16
|
+
* else.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Tools that work with no credential at all.
|
|
20
|
+
*
|
|
21
|
+
* Derived from `PUBLIC_PATHS` in `./http.js` — the endpoints the HTTP client is allowed to call
|
|
22
|
+
* with no bearer — plus the two tools that never touch our API: `verify_delegation` /
|
|
23
|
+
* `verify_delegations` run `@proof-holdings/delegation-verifier` against an issuer's public
|
|
24
|
+
* surfaces, and `render_auth_link` is local QR generation.
|
|
25
|
+
*
|
|
26
|
+
* `__tests__/auth-boundary.test.ts` compares this list against the tools the factory actually
|
|
27
|
+
* registers, in BOTH directions: a name here that no tool answers to would refuse nothing while
|
|
28
|
+
* reading exactly like a boundary, and anything the server registers that is not here must be
|
|
29
|
+
* refused without a key.
|
|
30
|
+
*/
|
|
31
|
+
export declare const KEYLESS_TOOLS: ReadonlySet<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Tools that WORK over a `start_login` session.
|
|
34
|
+
*
|
|
35
|
+
* Their endpoints are the caller's own account records — the paths `SESSION_AUTH_PREFIXES` in
|
|
36
|
+
* `./http.ts` signs with the session token (plus `/api/v1/auth/me`). Every other keyed tool needs
|
|
37
|
+
* the API key the client's Authenticate step grants, and the refusal below tells the caller which
|
|
38
|
+
* side of that line the tool they asked for is on, BEFORE they walk the road. It used to say
|
|
39
|
+
* "call start_login" to everyone; a human did, signed in through Telegram, and was refused again by
|
|
40
|
+
* a second text that finally explained the difference (h-fix-mcp-refusal-roads).
|
|
41
|
+
*
|
|
42
|
+
* Hand-written here because the package cannot read docs/api-map.yaml at runtime, and held equal to
|
|
43
|
+
* the truth in BOTH directions by `src/__tests__/drift/api-parity.test.ts` (`parseMcpSessionTools`
|
|
44
|
+
* joins this set to the map through the paths the client signs) and by `validateMcpSessionToolCoverage`
|
|
45
|
+
* in `scripts/validate-api-map.ts`. `__tests__/auth-boundary.test.ts` keeps it to registered tools
|
|
46
|
+
* and disjoint from `KEYLESS_TOOLS`.
|
|
47
|
+
*/
|
|
48
|
+
export declare const SESSION_TOOLS: ReadonlySet<string>;
|
|
49
|
+
/**
|
|
50
|
+
* What a caller must do to open a tool, in the two channels a client delivers ANONYMOUSLY and
|
|
51
|
+
* ALWAYS: the descriptions in `tools/list` and the `instructions` of the `initialize` result.
|
|
52
|
+
*
|
|
53
|
+
* WHY THE REFUSAL BELOW IS NOT ENOUGH, measured rather than reasoned. The 401 + `WWW-Authenticate`
|
|
54
|
+
* is correct and stays — it is the entire reason an OAuth-capable client offers to authenticate. But
|
|
55
|
+
* an MCP client raises that 401 as a TRANSPORT error: it throws before the body becomes a tool
|
|
56
|
+
* result, so `authorizationRequiredBody` reaches probes and developers and never reaches the model.
|
|
57
|
+
* On a live Claude Code session that gap was filled by the client's own guess — "the token expired",
|
|
58
|
+
* said about a server that had never been handed a token. Returning the refusal as a plain result
|
|
59
|
+
* instead would fix the reading and break the button (`docs/mcp-oauth-client-probe.md`), so the fact
|
|
60
|
+
* is stated a second time, up front, where it is always readable.
|
|
61
|
+
*
|
|
62
|
+
* ONLY WHILE IT IS TRUE. With a credential in hand every notice would be noise, charged to the
|
|
63
|
+
* agent's context on every `tools/list` for every tool — so a keyed server adds nothing at all.
|
|
64
|
+
*/
|
|
65
|
+
export interface AccessContext {
|
|
66
|
+
/** The Streamable HTTP entrypoint. Changes the recipe, exactly as it does for `HttpClient`. */
|
|
67
|
+
remote?: boolean;
|
|
68
|
+
/** Whether this server was built with an API key. */
|
|
69
|
+
hasCredential: boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The suffix appended to one tool's description, or `''` when the tool needs nothing.
|
|
73
|
+
*
|
|
74
|
+
* The session/keyed split is the same one `keyless_alternative` makes in the refusal, and it is
|
|
75
|
+
* here for the same reason: telling every refused caller to run `start_login` walked a human
|
|
76
|
+
* through an entire Telegram login and into a SECOND refusal that contradicted the first
|
|
77
|
+
* (`h-fix-mcp-refusal-roads`).
|
|
78
|
+
*/
|
|
79
|
+
export declare function accessNoticeFor(tool: string, context: AccessContext): string;
|
|
80
|
+
/**
|
|
81
|
+
* The `instructions` string of the `initialize` result — the one place the whole arrangement can be
|
|
82
|
+
* stated once instead of per tool.
|
|
83
|
+
*
|
|
84
|
+
* Whether a given client actually shows this to its model is NOT assumed here; it is measured on a
|
|
85
|
+
* live client as part of this task, and the per-tool notice above is what carries the fact when the
|
|
86
|
+
* answer is no.
|
|
87
|
+
*/
|
|
88
|
+
export declare function serverInstructions(context: AccessContext): string;
|
|
89
|
+
/**
|
|
90
|
+
* RFC 9728 — the document our `WWW-Authenticate` names in `resource_metadata`.
|
|
91
|
+
*
|
|
92
|
+
* A DELIBERATE SECOND COPY of `PROTECTED_RESOURCE_METADATA_PATH` in the backend's
|
|
93
|
+
* `src/constants/mcpOauth.ts`. This package is its own build and published separately, so it cannot
|
|
94
|
+
* import from `src/`; the two are held together by text instead —
|
|
95
|
+
* `src/__tests__/drift/mcp-oauth-remote-wiring.test.ts` reads this literal out of this file and
|
|
96
|
+
* compares it with the constant. Rename either side without the other and that suite goes red,
|
|
97
|
+
* which is the only thing standing between "the header points at a document" and "the header points
|
|
98
|
+
* at a 404 while every test is green".
|
|
99
|
+
*/
|
|
100
|
+
export declare const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
101
|
+
/** Absolute address of the protected-resource document for a given API origin. */
|
|
102
|
+
export declare function resourceMetadataUrl(baseUrl: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* The `WWW-Authenticate` value. Form taken from the run that actually worked — all three clients
|
|
105
|
+
* followed `resource_metadata` from this exact shape (`mcp/probe/oauth-probe.mjs`).
|
|
106
|
+
*/
|
|
107
|
+
export declare function authorizationChallenge(baseUrl: string): string;
|
|
108
|
+
export interface AuthorizationRequiredBody {
|
|
109
|
+
error: 'authorization_required';
|
|
110
|
+
error_description: string;
|
|
111
|
+
/**
|
|
112
|
+
* `GET /api/v1/mcp/connect` — the machine-readable connection document, for the CLIENT. It was
|
|
113
|
+
* called `login_url` until a human opened it in a browser expecting a sign-in page and got raw
|
|
114
|
+
* JSON. There is no page that connects an MCP client: connecting happens inside the client.
|
|
115
|
+
*/
|
|
116
|
+
connect_instructions_url: string;
|
|
117
|
+
/**
|
|
118
|
+
* What `start_login` gets this caller — true PER TOOL. For a tool in `SESSION_TOOLS` it is a
|
|
119
|
+
* working road; for any other keyed tool it says so plainly, instead of sending them down it.
|
|
120
|
+
*/
|
|
121
|
+
keyless_alternative: string;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Stands in for the name of a `tools/call` that did not give one. It never matches a real tool, so
|
|
125
|
+
* it cannot collide with `KEYLESS_TOOLS`, and the refusal below recognises it.
|
|
126
|
+
*/
|
|
127
|
+
export declare const MALFORMED_TOOL_NAME = "<unnamed>";
|
|
128
|
+
/**
|
|
129
|
+
* The refusal for a `tools/call` that named no tool.
|
|
130
|
+
*
|
|
131
|
+
* Its own body because it is its own situation: such a call is classified KEYED (fail-closed), but
|
|
132
|
+
* answering it with "the tool <unnamed> needs a Proof account. Sign in…" would send somebody whose
|
|
133
|
+
* message was malformed off to authenticate, which fixes nothing. The 401 and the challenge stay —
|
|
134
|
+
* the classification is what they follow from — while the words say what is actually wrong.
|
|
135
|
+
*/
|
|
136
|
+
export declare function malformedToolCallBody(): {
|
|
137
|
+
error: 'invalid_request';
|
|
138
|
+
error_description: string;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* The human half of the refusal, written for the WORST measured client rather than the best.
|
|
142
|
+
*
|
|
143
|
+
* MCP Inspector replays the call by itself and Claude Desktop does it after one button press, so
|
|
144
|
+
* neither user ever reads this. Claude Code turns toward OAuth, stops before the browser step,
|
|
145
|
+
* needs a separate `claude mcp login`, and does NOT replay the call — for that person this text is
|
|
146
|
+
* the entire instruction, which is why it names the action twice over rather than reporting a
|
|
147
|
+
* state.
|
|
148
|
+
*
|
|
149
|
+
* `connect_instructions_url` is the existing public `GET /api/v1/mcp/connect` (`h-mcp-showcase`) rather than a new
|
|
150
|
+
* address. Nothing here makes a network call: `start_login` would be the more direct link, but it
|
|
151
|
+
* CREATES a login session, so calling it from a 401 handler would hand any anonymous caller a
|
|
152
|
+
* generator of Redis entries. It is named in the text instead, for clients that cannot speak OAuth
|
|
153
|
+
* at all.
|
|
154
|
+
*/
|
|
155
|
+
export declare function authorizationRequiredBody(baseUrl: string, tool: string): AuthorizationRequiredBody;
|
|
156
|
+
/**
|
|
157
|
+
* The refusal for a request whose credential does not match the session it names.
|
|
158
|
+
*
|
|
159
|
+
* A separate text because it is a different situation, not a different tool: nothing the caller
|
|
160
|
+
* asked for is wrong, the session id and the token simply belong to two different connections. The
|
|
161
|
+
* challenge beside it is the same, so a client that lost its token still turns toward the
|
|
162
|
+
* authorization server instead of retrying forever.
|
|
163
|
+
*/
|
|
164
|
+
export declare function sessionCredentialMismatchBody(): {
|
|
165
|
+
error: 'authorization_required';
|
|
166
|
+
error_description: string;
|
|
167
|
+
};
|
|
168
|
+
/** One tool call that needs an account. `malformed` means the call named no tool at all. */
|
|
169
|
+
export interface KeyedToolCall {
|
|
170
|
+
name: string;
|
|
171
|
+
malformed: boolean;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Names of the tool calls in this JSON-RPC message that need an account.
|
|
175
|
+
*
|
|
176
|
+
* FAIL-CLOSED on every axis: a name that is not in `KEYLESS_TOOLS` is keyed, including one that
|
|
177
|
+
* matches no tool at all — a typo must break a tool rather than open a hole. Anything that is not a
|
|
178
|
+
* `tools/call` (the handshake, `tools/list`, notifications, an unparseable body) contributes
|
|
179
|
+
* nothing, because refusing those is the arrangement the probe measured as a broken connection.
|
|
180
|
+
*
|
|
181
|
+
* `registered`, when the caller holds it, is the set of names the session's server answers to. A
|
|
182
|
+
* well-formed name outside it is then skipped rather than keyed, so the SDK refuses it as an unknown
|
|
183
|
+
* tool instead of the caller being sent to sign in over a typo (M9, live review 2026-09-26). That
|
|
184
|
+
* opens nothing — no registered tool answers to such a name — and the typo protection above still
|
|
185
|
+
* holds, since a keyed tool missing from `KEYLESS_TOOLS` is registered and therefore still keyed.
|
|
186
|
+
* One bound, stated rather than implied: the SDK looks the name up in a plain object, so an
|
|
187
|
+
* inherited name (`constructor`, `toString`) resolves to a prototype member, not to nothing, and what
|
|
188
|
+
* refuses it is the SDK's own `enabled` check ("Tool constructor disabled") — a property of the
|
|
189
|
+
* dependency, not of this gate.
|
|
190
|
+
* Without the set (the opening request, before any server exists) the rule stays fully fail-closed.
|
|
191
|
+
*/
|
|
192
|
+
export declare function keyedToolsIn(body: unknown, registered?: ReadonlySet<string>): KeyedToolCall[];
|
|
193
|
+
//# sourceMappingURL=authBoundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authBoundary.d.ts","sourceRoot":"","sources":["../src/authBoundary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,EAAE,WAAW,CAAC,MAAM,CAc5C,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,EAAE,WAAW,CAAC,MAAM,CAuD5C,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,aAAa;IAC5B,+FAA+F;IAC/F,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qDAAqD;IACrD,aAAa,EAAE,OAAO,CAAC;CACxB;AAwBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,MAAM,CAQ5E;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CA4CjE;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,0CAA0C,CAAC;AAExF,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,wBAAwB,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,wBAAwB,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAE/C;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI;IAAE,KAAK,EAAE,iBAAiB,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAO/F;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,yBAAyB,CAmBlG;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,IAAI;IAAE,KAAK,EAAE,wBAAwB,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAO9G;AAED,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,aAAa,EAAE,CA2B7F"}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The boundary between what this server answers with no account and what needs one
|
|
3
|
+
* (`h-mcp-oauth-remote-wiring`).
|
|
4
|
+
*
|
|
5
|
+
* WHY IT LIVES HERE, and not inside a tool handler: the answer to "you need to sign in" is an HTTP
|
|
6
|
+
* 401 carrying `WWW-Authenticate`, and that is what a client's OAuth stack reads. By the time a tool
|
|
7
|
+
* handler runs, the response is already a JSON-RPC result — the same refusal delivered there is
|
|
8
|
+
* text, not a challenge, and every measured client ignores it (`docs/mcp-oauth-client-probe.md`).
|
|
9
|
+
* So the remote server has to classify the message BEFORE handing it to the transport, and this
|
|
10
|
+
* module is the one place that classification is written down.
|
|
11
|
+
*
|
|
12
|
+
* WHERE IT SITS, and why not on `initialize`: the control measurement in the probe put a 401 on the
|
|
13
|
+
* opening handshake and the human saw `Failed to connect — connection timed out after 30000ms`,
|
|
14
|
+
* i.e. a broken connection rather than an invitation to sign in. Anonymous connection is a closed
|
|
15
|
+
* criterion of `h-mcp-remote`, so the 401 lands on the TOOL CALL that needs an account and nowhere
|
|
16
|
+
* else.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Tools that work with no credential at all.
|
|
20
|
+
*
|
|
21
|
+
* Derived from `PUBLIC_PATHS` in `./http.js` — the endpoints the HTTP client is allowed to call
|
|
22
|
+
* with no bearer — plus the two tools that never touch our API: `verify_delegation` /
|
|
23
|
+
* `verify_delegations` run `@proof-holdings/delegation-verifier` against an issuer's public
|
|
24
|
+
* surfaces, and `render_auth_link` is local QR generation.
|
|
25
|
+
*
|
|
26
|
+
* `__tests__/auth-boundary.test.ts` compares this list against the tools the factory actually
|
|
27
|
+
* registers, in BOTH directions: a name here that no tool answers to would refuse nothing while
|
|
28
|
+
* reading exactly like a boundary, and anything the server registers that is not here must be
|
|
29
|
+
* refused without a key.
|
|
30
|
+
*/
|
|
31
|
+
export const KEYLESS_TOOLS = new Set([
|
|
32
|
+
// Bootstrap: create an account and sign in, which is how a keyless caller stops being keyless.
|
|
33
|
+
'create_account',
|
|
34
|
+
'send_account_email',
|
|
35
|
+
'wait_for_account_creation',
|
|
36
|
+
'start_login',
|
|
37
|
+
'wait_for_login',
|
|
38
|
+
// Public verification surface: checking somebody else's proof is not an account operation.
|
|
39
|
+
'validate_proof',
|
|
40
|
+
'list_revoked_proofs',
|
|
41
|
+
'verify_delegation',
|
|
42
|
+
'verify_delegations',
|
|
43
|
+
// Local computation, no HTTP at all.
|
|
44
|
+
'render_auth_link',
|
|
45
|
+
]);
|
|
46
|
+
/**
|
|
47
|
+
* Tools that WORK over a `start_login` session.
|
|
48
|
+
*
|
|
49
|
+
* Their endpoints are the caller's own account records — the paths `SESSION_AUTH_PREFIXES` in
|
|
50
|
+
* `./http.ts` signs with the session token (plus `/api/v1/auth/me`). Every other keyed tool needs
|
|
51
|
+
* the API key the client's Authenticate step grants, and the refusal below tells the caller which
|
|
52
|
+
* side of that line the tool they asked for is on, BEFORE they walk the road. It used to say
|
|
53
|
+
* "call start_login" to everyone; a human did, signed in through Telegram, and was refused again by
|
|
54
|
+
* a second text that finally explained the difference (h-fix-mcp-refusal-roads).
|
|
55
|
+
*
|
|
56
|
+
* Hand-written here because the package cannot read docs/api-map.yaml at runtime, and held equal to
|
|
57
|
+
* the truth in BOTH directions by `src/__tests__/drift/api-parity.test.ts` (`parseMcpSessionTools`
|
|
58
|
+
* joins this set to the map through the paths the client signs) and by `validateMcpSessionToolCoverage`
|
|
59
|
+
* in `scripts/validate-api-map.ts`. `__tests__/auth-boundary.test.ts` keeps it to registered tools
|
|
60
|
+
* and disjoint from `KEYLESS_TOOLS`.
|
|
61
|
+
*/
|
|
62
|
+
export const SESSION_TOOLS = new Set([
|
|
63
|
+
'add_domain',
|
|
64
|
+
'add_verification_provider',
|
|
65
|
+
'cancel_user_request',
|
|
66
|
+
'check_domain_credentials',
|
|
67
|
+
'check_domain_email_status',
|
|
68
|
+
'check_user_domain_verification',
|
|
69
|
+
'claim_request_assets',
|
|
70
|
+
'claim_username',
|
|
71
|
+
'confirm_domain_email_code',
|
|
72
|
+
'connect_cloudflare',
|
|
73
|
+
'connect_dns_provider',
|
|
74
|
+
'connect_godaddy',
|
|
75
|
+
'create_api_key',
|
|
76
|
+
'create_dns_credential',
|
|
77
|
+
'create_user_request',
|
|
78
|
+
'delete_dns_credential',
|
|
79
|
+
'delete_domain',
|
|
80
|
+
'extend_request',
|
|
81
|
+
'get_2fa_status',
|
|
82
|
+
'get_add_email_status',
|
|
83
|
+
'get_add_phone_status',
|
|
84
|
+
'get_api_key_usage',
|
|
85
|
+
'get_current_user',
|
|
86
|
+
'get_dns_providers',
|
|
87
|
+
'get_domain',
|
|
88
|
+
'get_user_domain_verification_status',
|
|
89
|
+
'list_api_keys',
|
|
90
|
+
'list_dns_credentials',
|
|
91
|
+
'list_domains',
|
|
92
|
+
'list_emails',
|
|
93
|
+
'list_incoming_requests',
|
|
94
|
+
'list_my_requests',
|
|
95
|
+
'list_phones',
|
|
96
|
+
'regenerate_api_key',
|
|
97
|
+
'remove_email',
|
|
98
|
+
'remove_phone',
|
|
99
|
+
'resend_domain_email',
|
|
100
|
+
'resend_email_otp',
|
|
101
|
+
'revoke_api_key',
|
|
102
|
+
'set_primary_email',
|
|
103
|
+
'set_primary_phone',
|
|
104
|
+
'setup_domain_email',
|
|
105
|
+
'share_request_email',
|
|
106
|
+
'start_2fa',
|
|
107
|
+
'start_add_email',
|
|
108
|
+
'start_add_phone',
|
|
109
|
+
'start_domain_email_verification',
|
|
110
|
+
'start_user_domain_verification',
|
|
111
|
+
'update_public_proofs',
|
|
112
|
+
'verify_2fa',
|
|
113
|
+
'verify_2fa_magic_link',
|
|
114
|
+
'verify_domain',
|
|
115
|
+
'verify_domain_with_credentials',
|
|
116
|
+
'verify_email_otp',
|
|
117
|
+
]);
|
|
118
|
+
/** How to get an account here — the one sentence that differs between the two transports. */
|
|
119
|
+
function credentialRecipe(remote) {
|
|
120
|
+
return remote
|
|
121
|
+
? 'authenticate this client (in Claude Code: /mcp → this server → Authenticate)'
|
|
122
|
+
: "set PROOF_API_KEY in this server's environment and restart it";
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The same recipe as an IMPERATIVE, for the per-tool notice.
|
|
126
|
+
*
|
|
127
|
+
* Separate from the sentence above because every registration outside `KEYLESS_TOOLS` pays for it.
|
|
128
|
+
* Measured on the built server:
|
|
129
|
+
* the full-prose form added 33.8 KB to 41.8 KB of descriptions — it doubled the anonymous
|
|
130
|
+
* `tools/list`, which is the listing this whole epic exists to serve. The recipe survives; the
|
|
131
|
+
* padding does not, and `instructions` carries the long form once.
|
|
132
|
+
*/
|
|
133
|
+
function shortRecipe(remote) {
|
|
134
|
+
return remote
|
|
135
|
+
? 'Authenticate this client (Claude Code: /mcp → Authenticate)'
|
|
136
|
+
: 'Set PROOF_API_KEY and restart this server';
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The suffix appended to one tool's description, or `''` when the tool needs nothing.
|
|
140
|
+
*
|
|
141
|
+
* The session/keyed split is the same one `keyless_alternative` makes in the refusal, and it is
|
|
142
|
+
* here for the same reason: telling every refused caller to run `start_login` walked a human
|
|
143
|
+
* through an entire Telegram login and into a SECOND refusal that contradicted the first
|
|
144
|
+
* (`h-fix-mcp-refusal-roads`).
|
|
145
|
+
*/
|
|
146
|
+
export function accessNoticeFor(tool, context) {
|
|
147
|
+
if (context.hasCredential)
|
|
148
|
+
return '';
|
|
149
|
+
if (KEYLESS_TOOLS.has(tool))
|
|
150
|
+
return '';
|
|
151
|
+
const recipe = shortRecipe(context.remote);
|
|
152
|
+
return SESSION_TOOLS.has(tool)
|
|
153
|
+
? `\n\nACCESS: needs a Proof account. ${recipe}, or sign in with start_login — a session opens this tool — then call this tool again.`
|
|
154
|
+
: `\n\nACCESS: needs a Proof account. ${recipe}, then call this tool again. start_login does NOT open this tool.`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The `instructions` string of the `initialize` result — the one place the whole arrangement can be
|
|
158
|
+
* stated once instead of per tool.
|
|
159
|
+
*
|
|
160
|
+
* Whether a given client actually shows this to its model is NOT assumed here; it is measured on a
|
|
161
|
+
* live client as part of this task, and the per-tool notice above is what carries the fact when the
|
|
162
|
+
* answer is no.
|
|
163
|
+
*/
|
|
164
|
+
export function serverInstructions(context) {
|
|
165
|
+
const opening = 'Proof — identity and control verification (proof.holdings): verification requests across SMS, ' +
|
|
166
|
+
'messenger and biometric channels, human approvals, proof tokens, delegations and public profiles.';
|
|
167
|
+
if (context.hasCredential) {
|
|
168
|
+
return (`${opening} This connection is authenticated. Which tools work depends on the SCOPES of the key it ` +
|
|
169
|
+
'carries, not on this server: a tool outside them answers 403 insufficient_scope naming the scope it ' +
|
|
170
|
+
'wanted, which is a different problem from needing an account.');
|
|
171
|
+
}
|
|
172
|
+
// DERIVED from the set rather than retyped beside it: the keyless surface has widened three times
|
|
173
|
+
// in this epic, and a prose list would go on claiming an account is needed for a tool that needs
|
|
174
|
+
// none, with every gate green. Nothing hand-written describes the set either — a sentence
|
|
175
|
+
// categorising what these tools "cover" is true of today's ten and silently false of the
|
|
176
|
+
// eleventh, which is the same drift one clause later.
|
|
177
|
+
const keyless = [...KEYLESS_TOOLS].sort().join(', ');
|
|
178
|
+
// The 401 paragraph is REMOTE-ONLY. On stdio there is no HTTP layer to answer 401: the refusal is
|
|
179
|
+
// an `api_key_required` the handler returns as readable text, so an opaque failure there really is
|
|
180
|
+
// a crashed process or a call that outlived the client's timeout. Telling the agent otherwise
|
|
181
|
+
// would hand it a pre-written wrong cause — the same defect as "the token expired".
|
|
182
|
+
const opaqueFailure = context.remote
|
|
183
|
+
? 'If a tool call fails at the transport with no readable explanation, the usual cause is this server ' +
|
|
184
|
+
'answering 401 to ask for authentication — the refusal carries its reason in a body most clients ' +
|
|
185
|
+
'discard, so no expired token is involved: authenticate, then make the call again. A session left ' +
|
|
186
|
+
'idle for a long time is the other cause: this server drops idle sessions, and the next call on a ' +
|
|
187
|
+
'dropped one needs a reconnect rather than a credential.'
|
|
188
|
+
: 'A tool that needs an account answers a readable `api_key_required` result rather than failing, so ' +
|
|
189
|
+
'an opaque failure here is not a credential problem — it is a crashed server process or a call that ' +
|
|
190
|
+
"outlived your client's request timeout.";
|
|
191
|
+
return [
|
|
192
|
+
`${opening} This connection is ANONYMOUS — nothing has been signed in yet.`,
|
|
193
|
+
`Works right now with no account: ${keyless}.`,
|
|
194
|
+
`Everything else needs a Proof account, and each such tool says so in its own description. Two roads: ${credentialRecipe(context.remote)} and then CALL THE TOOL AGAIN — some clients replay it for you, Claude Code does not. That grants an API key, whose SCOPES then decide which tools work (a tool outside them answers 403 insufficient_scope, not a request to sign in). Or call start_login and wait_for_login, which signs ` +
|
|
195
|
+
'in a session that opens only your own account records — emails, phones, domains, API keys, 2FA, DNS ' +
|
|
196
|
+
"credentials, verification requests, your public profile's username and proof list.",
|
|
197
|
+
opaqueFailure,
|
|
198
|
+
].join('\n\n');
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* RFC 9728 — the document our `WWW-Authenticate` names in `resource_metadata`.
|
|
202
|
+
*
|
|
203
|
+
* A DELIBERATE SECOND COPY of `PROTECTED_RESOURCE_METADATA_PATH` in the backend's
|
|
204
|
+
* `src/constants/mcpOauth.ts`. This package is its own build and published separately, so it cannot
|
|
205
|
+
* import from `src/`; the two are held together by text instead —
|
|
206
|
+
* `src/__tests__/drift/mcp-oauth-remote-wiring.test.ts` reads this literal out of this file and
|
|
207
|
+
* compares it with the constant. Rename either side without the other and that suite goes red,
|
|
208
|
+
* which is the only thing standing between "the header points at a document" and "the header points
|
|
209
|
+
* at a 404 while every test is green".
|
|
210
|
+
*/
|
|
211
|
+
export const PROTECTED_RESOURCE_METADATA_PATH = '/.well-known/oauth-protected-resource';
|
|
212
|
+
/** Absolute address of the protected-resource document for a given API origin. */
|
|
213
|
+
export function resourceMetadataUrl(baseUrl) {
|
|
214
|
+
return `${baseUrl.replace(/\/+$/, '')}${PROTECTED_RESOURCE_METADATA_PATH}`;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The `WWW-Authenticate` value. Form taken from the run that actually worked — all three clients
|
|
218
|
+
* followed `resource_metadata` from this exact shape (`mcp/probe/oauth-probe.mjs`).
|
|
219
|
+
*/
|
|
220
|
+
export function authorizationChallenge(baseUrl) {
|
|
221
|
+
return `Bearer realm="proof", resource_metadata="${resourceMetadataUrl(baseUrl)}"`;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Stands in for the name of a `tools/call` that did not give one. It never matches a real tool, so
|
|
225
|
+
* it cannot collide with `KEYLESS_TOOLS`, and the refusal below recognises it.
|
|
226
|
+
*/
|
|
227
|
+
export const MALFORMED_TOOL_NAME = '<unnamed>';
|
|
228
|
+
/**
|
|
229
|
+
* The refusal for a `tools/call` that named no tool.
|
|
230
|
+
*
|
|
231
|
+
* Its own body because it is its own situation: such a call is classified KEYED (fail-closed), but
|
|
232
|
+
* answering it with "the tool <unnamed> needs a Proof account. Sign in…" would send somebody whose
|
|
233
|
+
* message was malformed off to authenticate, which fixes nothing. The 401 and the challenge stay —
|
|
234
|
+
* the classification is what they follow from — while the words say what is actually wrong.
|
|
235
|
+
*/
|
|
236
|
+
export function malformedToolCallBody() {
|
|
237
|
+
return {
|
|
238
|
+
error: 'invalid_request',
|
|
239
|
+
error_description: 'This tools/call named no tool, so there is nothing to run — and nothing to authorize either. ' +
|
|
240
|
+
'Send params.name with the tool you meant.',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The human half of the refusal, written for the WORST measured client rather than the best.
|
|
245
|
+
*
|
|
246
|
+
* MCP Inspector replays the call by itself and Claude Desktop does it after one button press, so
|
|
247
|
+
* neither user ever reads this. Claude Code turns toward OAuth, stops before the browser step,
|
|
248
|
+
* needs a separate `claude mcp login`, and does NOT replay the call — for that person this text is
|
|
249
|
+
* the entire instruction, which is why it names the action twice over rather than reporting a
|
|
250
|
+
* state.
|
|
251
|
+
*
|
|
252
|
+
* `connect_instructions_url` is the existing public `GET /api/v1/mcp/connect` (`h-mcp-showcase`) rather than a new
|
|
253
|
+
* address. Nothing here makes a network call: `start_login` would be the more direct link, but it
|
|
254
|
+
* CREATES a login session, so calling it from a 401 handler would hand any anonymous caller a
|
|
255
|
+
* generator of Redis entries. It is named in the text instead, for clients that cannot speak OAuth
|
|
256
|
+
* at all.
|
|
257
|
+
*/
|
|
258
|
+
export function authorizationRequiredBody(baseUrl, tool) {
|
|
259
|
+
const sessionReaches = SESSION_TOOLS.has(tool);
|
|
260
|
+
return {
|
|
261
|
+
error: 'authorization_required',
|
|
262
|
+
error_description: `The tool "${tool}" needs a Proof account. Sign in, then CALL THE TOOL AGAIN — some clients ` +
|
|
263
|
+
'replay the call for you, Claude Code does not. Connecting happens inside the client; there is ' +
|
|
264
|
+
'no web page for it. In Claude Code: /mcp → this server → Authenticate, or `claude mcp login ' +
|
|
265
|
+
'<server>` in a separate terminal window (it needs an interactive terminal). Then repeat the ' +
|
|
266
|
+
'call yourself.',
|
|
267
|
+
connect_instructions_url: `${baseUrl.replace(/\/+$/, '')}/api/v1/mcp/connect`,
|
|
268
|
+
keyless_alternative: sessionReaches
|
|
269
|
+
? 'No OAuth support in your client? Call start_login — it returns a sign-in link, and ' +
|
|
270
|
+
'render_auth_link turns that link into a QR code — then wait_for_login. This tool works over ' +
|
|
271
|
+
'that signed-in session.'
|
|
272
|
+
: 'start_login will NOT unlock this tool. A signed-in session covers only your own account ' +
|
|
273
|
+
'records — emails, phones, domains, API keys, 2FA, DNS credentials, verification requests, ' +
|
|
274
|
+
"your public profile's username and proof list. This tool needs the API key that connecting grants.",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The refusal for a request whose credential does not match the session it names.
|
|
279
|
+
*
|
|
280
|
+
* A separate text because it is a different situation, not a different tool: nothing the caller
|
|
281
|
+
* asked for is wrong, the session id and the token simply belong to two different connections. The
|
|
282
|
+
* challenge beside it is the same, so a client that lost its token still turns toward the
|
|
283
|
+
* authorization server instead of retrying forever.
|
|
284
|
+
*/
|
|
285
|
+
export function sessionCredentialMismatchBody() {
|
|
286
|
+
return {
|
|
287
|
+
error: 'authorization_required',
|
|
288
|
+
error_description: 'This session was opened with a different credential. A session belongs to the token that ' +
|
|
289
|
+
'opened it — reconnect to start a new one rather than reusing this session id.',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Names of the tool calls in this JSON-RPC message that need an account.
|
|
294
|
+
*
|
|
295
|
+
* FAIL-CLOSED on every axis: a name that is not in `KEYLESS_TOOLS` is keyed, including one that
|
|
296
|
+
* matches no tool at all — a typo must break a tool rather than open a hole. Anything that is not a
|
|
297
|
+
* `tools/call` (the handshake, `tools/list`, notifications, an unparseable body) contributes
|
|
298
|
+
* nothing, because refusing those is the arrangement the probe measured as a broken connection.
|
|
299
|
+
*
|
|
300
|
+
* `registered`, when the caller holds it, is the set of names the session's server answers to. A
|
|
301
|
+
* well-formed name outside it is then skipped rather than keyed, so the SDK refuses it as an unknown
|
|
302
|
+
* tool instead of the caller being sent to sign in over a typo (M9, live review 2026-09-26). That
|
|
303
|
+
* opens nothing — no registered tool answers to such a name — and the typo protection above still
|
|
304
|
+
* holds, since a keyed tool missing from `KEYLESS_TOOLS` is registered and therefore still keyed.
|
|
305
|
+
* One bound, stated rather than implied: the SDK looks the name up in a plain object, so an
|
|
306
|
+
* inherited name (`constructor`, `toString`) resolves to a prototype member, not to nothing, and what
|
|
307
|
+
* refuses it is the SDK's own `enabled` check ("Tool constructor disabled") — a property of the
|
|
308
|
+
* dependency, not of this gate.
|
|
309
|
+
* Without the set (the opening request, before any server exists) the rule stays fully fail-closed.
|
|
310
|
+
*/
|
|
311
|
+
export function keyedToolsIn(body, registered) {
|
|
312
|
+
const messages = Array.isArray(body) ? body : [body];
|
|
313
|
+
const keyed = [];
|
|
314
|
+
for (const message of messages) {
|
|
315
|
+
const entry = message;
|
|
316
|
+
if (!entry || typeof entry !== 'object')
|
|
317
|
+
continue;
|
|
318
|
+
if (entry.method !== 'tools/call')
|
|
319
|
+
continue;
|
|
320
|
+
const name = entry.params?.name;
|
|
321
|
+
// A `tools/call` whose name is missing or not a string is KEYED, under a placeholder —
|
|
322
|
+
// `continue` would have made a malformed call the one shape that walks past the gate.
|
|
323
|
+
//
|
|
324
|
+
// REACHABLE, and stated so rather than waved away: on the remote transport this classification
|
|
325
|
+
// runs BEFORE the message reaches the SDK, so the SDK's schema never gets to refuse it. Which
|
|
326
|
+
// is why "malformed" is carried as a FLAG rather than inferred later from the placeholder
|
|
327
|
+
// string: a caller may send `params.name: "<unnamed>"`, and deciding at the refusal by
|
|
328
|
+
// comparing strings would answer that caller "this call named no tool" — a sentence about a
|
|
329
|
+
// message they did not send. The decision belongs where the evidence is.
|
|
330
|
+
if (typeof name !== 'string' || !name) {
|
|
331
|
+
keyed.push({ name: MALFORMED_TOOL_NAME, malformed: true });
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (registered && !registered.has(name))
|
|
335
|
+
continue;
|
|
336
|
+
if (!KEYLESS_TOOLS.has(name))
|
|
337
|
+
keyed.push({ name, malformed: false });
|
|
338
|
+
}
|
|
339
|
+
return keyed;
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=authBoundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authBoundary.js","sourceRoot":"","sources":["../src/authBoundary.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { HttpClient } from './http.js';
|
|
3
|
+
export interface ProofServerOptions {
|
|
4
|
+
/** API key (Bearer). Absent → public mode, exactly as on the stdio path. */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
/**
|
|
8
|
+
* Set by the REMOTE entrypoint. The only thing it changes is what a "you need a key" refusal
|
|
9
|
+
* tells the user to do: the stdio instruction (set `PROOF_API_KEY`, restart the server) is
|
|
10
|
+
* impossible on a server run by somebody else.
|
|
11
|
+
*/
|
|
12
|
+
remote?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ProofServerInstance {
|
|
15
|
+
server: McpServer;
|
|
16
|
+
http: HttpClient;
|
|
17
|
+
/** Every tool name registered on `server`, collected by the registrar below as it registers them. */
|
|
18
|
+
toolNames: ReadonlySet<string>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Builds one MCP server with its own HTTP client.
|
|
22
|
+
*
|
|
23
|
+
* Why a factory at all (`h-mcp-remote`, SC-4): the stdio entrypoint serves exactly one user, so one
|
|
24
|
+
* `HttpClient` per process is correct there. The remote server serves many, and `HttpClient` keeps
|
|
25
|
+
* `sessionToken` / `refreshToken` as INSTANCE fields written by `captureSetCookie` — sharing one
|
|
26
|
+
* across connections would hand the next user the previous user's logged-in session. So the unit of
|
|
27
|
+
* isolation is the connection, and this function is that unit.
|
|
28
|
+
*
|
|
29
|
+
* The registration list below is the ONLY copy in the package: `server.ts` calls this rather than
|
|
30
|
+
* repeating it. A second copy would work on the day it was written and drift one module at a time
|
|
31
|
+
* afterwards, with both paths green — `__tests__/factory.test.ts` pins both the list's completeness
|
|
32
|
+
* against `src/tools` and the absence of a second copy in `server.ts`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createProofServer(options: ProofServerOptions): ProofServerInstance;
|
|
35
|
+
//# sourceMappingURL=factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAuB,MAAM,yCAAyC,CAAC;AAEzF,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAoCvC,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,UAAU,CAAC;IACjB,qGAAqG;IACrG,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAChC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,mBAAmB,CA0FlF"}
|