@truealter/sdk 0.5.3 → 0.5.8
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/README.md +92 -80
- package/dist/bin/mcp-bridge.js +141 -12
- package/dist/index.cjs +167 -23
- package/dist/index.d.cts +62 -39
- package/dist/index.d.ts +62 -39
- package/dist/index.js +167 -23
- package/package.json +3 -6
- package/dist/bin/alter-identity.js +0 -2641
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# @truealter/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
~Alter Identity SDK - query the continuous identity field from any JavaScript/TypeScript environment.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@truealter/sdk)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
[](https://glama.ai/mcp/servers/true-alter/alter-identity)
|
|
8
8
|
[](https://www.deepnlp.org/store/ai-agent/identity/pub-truealter/alter-identity)
|
|
9
9
|
|
|
10
|
-
A thin client over the
|
|
10
|
+
A thin client over the ~Alter MCP server (Streamable HTTP, JSON-RPC 2.0, MCP spec `2025-11-25`) with x402 micropayment support, ES256 provenance verification, and config generators for Claude Code, Cursor, and generic MCP clients.
|
|
11
11
|
|
|
12
12
|
- **Branded host:** `https://mcp.truealter.com` (serves `.well-known/mcp.json` for discovery)
|
|
13
13
|
- **JSON-RPC wire endpoint:** `https://mcp.truealter.com/api/v1/mcp` - this is what Streamable HTTP POSTs target (the SDK default)
|
|
14
14
|
- **Wire protocol:** Streamable HTTP, JSON-RPC 2.0, MCP `2025-11-25` (server negotiates `2025-06-18` + `2025-03-26` for backwards-compatible clients)
|
|
15
|
-
- **Tools:** **
|
|
15
|
+
- **Tools:** **36 publicly advertised**, 27 free (L0) + 9 premium (L1-L5), kept in sync with ~Alter's live MCP server at every publish.
|
|
16
16
|
- **Runtime:** Node 18+, Deno, Bun, Cloudflare Workers, modern browsers
|
|
17
17
|
- **Crypto:** `@noble/ed25519` + `@noble/hashes` (no other dependencies)
|
|
18
18
|
- **Bundle:** ESM + CJS dual output
|
|
@@ -21,41 +21,46 @@ A thin client over the ALTER MCP server (Streamable HTTP, JSON-RPC 2.0, MCP spec
|
|
|
21
21
|
|
|
22
22
|
```
|
|
23
23
|
npm install @truealter/sdk
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then import the client in your code (see the API section below). The
|
|
27
|
+
day-to-day command line lives in
|
|
28
|
+
[`@truealter/cli`](https://www.npmjs.com/package/@truealter/cli):
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
alter init
|
|
32
|
+
alter verify ~alter
|
|
26
33
|
```
|
|
27
34
|
|
|
28
35
|
## Bridge vs SDK
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
the
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
This package ships a stdio bridge entrypoint (`bin/mcp-bridge.ts`,
|
|
38
|
+
built to `dist/bin/mcp-bridge.js`) that the `alter` CLI launches by file
|
|
39
|
+
path via its `mcp-bridge` subcommand. It is a **dev/demo surface** for
|
|
40
|
+
dropping ~Alter into MCP hosts that speak the stdio transport (Claude Code,
|
|
41
|
+
Cursor, Continue, Windsurf). It is useful for handshake, `tools/list`, and
|
|
42
|
+
L0 tool calls, but it does not carry ES256 per-invocation signing:
|
|
43
|
+
authenticated MCP tools will fail at the server edge when reached through
|
|
44
|
+
the bridge. For production use, import `@truealter/sdk` directly and
|
|
45
|
+
construct an `MCPClient` / `AlterClient` with the optional `signing`
|
|
46
|
+
parameter; that path is the primary one and carries the provenance
|
|
47
|
+
envelope end-to-end. Bridge signing is planned for a future release.
|
|
40
48
|
|
|
41
49
|
## CLI
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
`alter
|
|
46
|
-
`
|
|
47
|
-
|
|
48
|
-
not part of this package.
|
|
49
|
-
|
|
50
|
-
Run `alter-identity --help` for the inline reference.
|
|
51
|
+
This package exposes no command-line binary of its own: it is a library you
|
|
52
|
+
import. The bridge entrypoint above is not a published `bin`; it is resolved
|
|
53
|
+
by file path from the `alter` CLI, which is distributed separately as
|
|
54
|
+
[`@truealter/cli`](https://www.npmjs.com/package/@truealter/cli). Run
|
|
55
|
+
`alter --help` for the inline reference.
|
|
51
56
|
|
|
52
|
-
## Why
|
|
57
|
+
## Why ~Alter is not IAM
|
|
53
58
|
|
|
54
|
-
Identity Access Management answers *who is logged in*.
|
|
59
|
+
Identity Access Management answers *who is logged in*. ~Alter answers *who they actually are* - a continuous field of recognition that any IAM stack can sit on top of.
|
|
55
60
|
|
|
56
61
|
## Theoretical Foundation
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
~Alter is the working instantiation of an eight-paper academic corpus on identity field theory. The SDK below is what happens when the theory ships as protocol. Each paper is open access on figshare under CC-BY 4.0.
|
|
59
64
|
|
|
60
65
|
| Paper | Title | DOI |
|
|
61
66
|
|-------|-------|-----|
|
|
@@ -89,7 +94,7 @@ const alter = new AlterClient({
|
|
|
89
94
|
|
|
90
95
|
### Minimum-version preflight (required)
|
|
91
96
|
|
|
92
|
-
|
|
97
|
+
~Alter's backend publishes a per-client minimum-version floor. The SDK
|
|
93
98
|
preflights this floor lazily on the first network call: no explicit
|
|
94
99
|
call is required for the common case. If the running SDK is below the
|
|
95
100
|
floor for `alter-identity`, the SDK throws `BelowFloorError` with the
|
|
@@ -167,9 +172,9 @@ identity headers that the server-side floor middleware consults:
|
|
|
167
172
|
| `X-Alter-Client-Version` | the running `SDK_VERSION` |
|
|
168
173
|
| `X-Alter-Client-Channel` | `npm` |
|
|
169
174
|
|
|
170
|
-
These are MANDATORY on every authenticated backend endpoint
|
|
171
|
-
|
|
172
|
-
and is NEVER used for floor enforcement.
|
|
175
|
+
These are MANDATORY on every authenticated backend endpoint so the
|
|
176
|
+
server can enforce its minimum supported client version. The User-Agent
|
|
177
|
+
header remains informational and is NEVER used for floor enforcement.
|
|
173
178
|
|
|
174
179
|
### Free tier (L0 - no payment required)
|
|
175
180
|
|
|
@@ -185,7 +190,7 @@ const verifiedById = await alter.verify(
|
|
|
185
190
|
},
|
|
186
191
|
);
|
|
187
192
|
|
|
188
|
-
// Reference data - the 12
|
|
193
|
+
// Reference data - the 12 ~Alter archetypes
|
|
189
194
|
const archetypes = await alter.listArchetypes();
|
|
190
195
|
|
|
191
196
|
// Identity depth and available tool tiers
|
|
@@ -307,7 +312,7 @@ Resulting `.mcp.json`:
|
|
|
307
312
|
"alter": {
|
|
308
313
|
"url": "https://mcp.truealter.com/api/v1/mcp",
|
|
309
314
|
"transport": "streamable-http",
|
|
310
|
-
"description": "
|
|
315
|
+
"description": "~Alter Identity - psychometric identity field for AI agents",
|
|
311
316
|
"headers": {
|
|
312
317
|
"X-ALTER-API-Key": "ak_..."
|
|
313
318
|
}
|
|
@@ -344,18 +349,21 @@ const config = generateGenericMcpConfig({
|
|
|
344
349
|
|
|
345
350
|
### CLI
|
|
346
351
|
|
|
352
|
+
The command line lives in [`@truealter/cli`](https://www.npmjs.com/package/@truealter/cli),
|
|
353
|
+
not in this SDK package:
|
|
354
|
+
|
|
347
355
|
```
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
356
|
+
alter init # generate keypair, discover MCP, write ~/.config/alter/identity.json
|
|
357
|
+
alter config # print Claude .mcp.json snippet (default)
|
|
358
|
+
alter config --cursor # print Cursor .cursor/mcp.json snippet
|
|
359
|
+
alter config --generic # print generic mcpServers snippet
|
|
360
|
+
alter verify ~alter # verify an identity
|
|
361
|
+
alter status # show connection state and probe the endpoint
|
|
354
362
|
```
|
|
355
363
|
|
|
356
364
|
## x402 Micropayments
|
|
357
365
|
|
|
358
|
-
|
|
366
|
+
~Alter monetises premium tools via the [x402](https://x402.org) standard - HTTP `402 Payment Required` with on-chain settlement.
|
|
359
367
|
|
|
360
368
|
### The retry flow
|
|
361
369
|
|
|
@@ -363,7 +371,7 @@ ALTER monetises premium tools via the [x402](https://x402.org) standard - HTTP `
|
|
|
363
371
|
2. Server replies `402 Payment Required` with a payment requirement (amount, recipient, asset, network).
|
|
364
372
|
3. Client signs and broadcasts a USDC transfer on Base L2, attaches the proof, retries.
|
|
365
373
|
4. Server validates the proof, executes the tool, signs the response with ES256, returns it.
|
|
366
|
-
5. AlterRouter executes the split on-chain in the same transaction. The data subject receives Identity Income directly;
|
|
374
|
+
5. AlterRouter executes the split on-chain in the same transaction. The data subject receives Identity Income directly; ~Alter receives only its protocol cut. No custodian, no broker.
|
|
367
375
|
|
|
368
376
|
The SDK handles steps 2-4 automatically when an `X402Client` with a configured `signer` is passed in.
|
|
369
377
|
|
|
@@ -426,7 +434,7 @@ const result = await alter.getFullTraitVector({
|
|
|
426
434
|
});
|
|
427
435
|
|
|
428
436
|
const check = await alter.verifyProvenance(result._meta?.provenance);
|
|
429
|
-
if (!check.valid) throw new Error(
|
|
437
|
+
if (!check.valid) throw new Error(`~alter provenance check failed: ${check.reason}`);
|
|
430
438
|
```
|
|
431
439
|
|
|
432
440
|
The SDK fetches public keys from `https://api.truealter.com/.well-known/alter-keys.json` and caches them per their `Cache-Control` headers. The endpoint returns a JWKS containing all current and recently-rotated signing keys; verifying clients should accept any key whose `kid` matches and is still within its validity window.
|
|
@@ -440,7 +448,7 @@ import { AlterClient, DEFAULT_VERIFY_AT_ALLOWLIST } from "@truealter/sdk";
|
|
|
440
448
|
|
|
441
449
|
const alter = new AlterClient({
|
|
442
450
|
verifyAtAllowlist: [
|
|
443
|
-
...DEFAULT_VERIFY_AT_ALLOWLIST, // keep the
|
|
451
|
+
...DEFAULT_VERIFY_AT_ALLOWLIST, // keep the ~Alter canonicals
|
|
444
452
|
"keys.myorg.example", // plus your own JWKS host
|
|
445
453
|
],
|
|
446
454
|
});
|
|
@@ -450,17 +458,17 @@ If you pin `jwksUrl` explicitly, the envelope's `verify_at` is ignored entirely
|
|
|
450
458
|
|
|
451
459
|
### Why this matters
|
|
452
460
|
|
|
453
|
-
Provenance verification is how Agent A trusts that data from Agent B truly came from
|
|
461
|
+
Provenance verification is how Agent A trusts that data from Agent B truly came from ~Alter. If Agent B forwards a trait vector or belonging score, Agent A can replay the JWS against ~Alter's published keys and confirm - without contacting ~Alter again - that the payload is authentic, untampered, and was issued for the person Agent B claims it concerns. No shared secret, no trust in the intermediary, no out-of-band coordination.
|
|
454
462
|
|
|
455
|
-
This is what makes
|
|
463
|
+
This is what makes ~alter usable as identity infrastructure rather than just an API: signed claims propagate across agent networks the same way DKIM-signed mail propagates across SMTP relays.
|
|
456
464
|
|
|
457
465
|
## Discovery
|
|
458
466
|
|
|
459
|
-
|
|
467
|
+
~Alter follows the discovery cascade specified in [draft-morrison-mcp-dns-discovery-01](https://datatracker.ietf.org/doc/draft-morrison-mcp-dns-discovery/). Given a domain (e.g. `truealter.com`), the SDK resolves the MCP endpoint in three steps, falling through on each failure:
|
|
460
468
|
|
|
461
469
|
1. **DNS TXT** - query `_mcp.truealter.com` for a TXT record of the form `mcp=https://mcp.truealter.com;version=2025-11-25`. This is the fastest path and works without an HTTP round-trip.
|
|
462
470
|
2. **`.well-known/mcp.json`** - fetch `https://truealter.com/.well-known/mcp.json` for the standard MCP server descriptor. This is the cross-vendor fallback.
|
|
463
|
-
3. **`.well-known/alter.json`** - fetch `https://truealter.com/.well-known/alter.json` for the
|
|
471
|
+
3. **`.well-known/alter.json`** - fetch `https://truealter.com/.well-known/alter.json` for the ~Alter-specific descriptor, including signing keys, x402 wallet address, supported tool tiers, and federation endpoints.
|
|
464
472
|
|
|
465
473
|
```ts
|
|
466
474
|
import { discover } from "@truealter/sdk";
|
|
@@ -480,45 +488,49 @@ This draft is the author's Internet-Draft (not yet adopted by an IETF working gr
|
|
|
480
488
|
|
|
481
489
|
| Name | Tier | Cost | Description |
|
|
482
490
|
|---------------------------|------|-------|----------------------------------------------------------------------------------------------------------------------|
|
|
483
|
-
| `hello_agent`
|
|
484
|
-
| `
|
|
485
|
-
| `
|
|
486
|
-
| `verify_identity`
|
|
487
|
-
| `
|
|
488
|
-
| `
|
|
489
|
-
| `
|
|
490
|
-
| `
|
|
491
|
-
| `
|
|
492
|
-
| `
|
|
493
|
-
| `
|
|
494
|
-
| `
|
|
495
|
-
| `
|
|
496
|
-
| `
|
|
497
|
-
| `
|
|
498
|
-
| `
|
|
499
|
-
| `
|
|
500
|
-
| `
|
|
501
|
-
| `
|
|
502
|
-
| `
|
|
503
|
-
| `
|
|
504
|
-
| `
|
|
505
|
-
| `
|
|
506
|
-
| `
|
|
491
|
+
| `hello_agent` | L0 | free | First handshake with ~Alter - returns server version, authentication status, your trust tier, and available tool counts. |
|
|
492
|
+
| `list_archetypes` | L0 | free | Returns archetype reference data. |
|
|
493
|
+
| `alter_resolve_handle` | L0 | free | Resolve a `~handle` (e.g. `~example`) to its canonical form and kind. No auth required - the handle-wedge entry point. |
|
|
494
|
+
| `verify_identity` | L0 | free | Verify whether a person is registered with ~Alter and validate optional identity claims. |
|
|
495
|
+
| `alter_presence_read` | L0 | free | Read whether a `~handle` is publicly open, the shop-front sign. Returns open or closed only; the closed reason is never disclosed. |
|
|
496
|
+
| `alter_resolve_by_key` | L0 | free | Resolve a paired third-party key (email or OAuth user-id) to its bound `~handle`, gated by the member's per-stream resolver opt-in. |
|
|
497
|
+
| `get_engagement_level` | L0 | free | Get a person's identity depth - engagement level, data quality tier, and available query tiers. |
|
|
498
|
+
| `get_profile` | L0 | free | Get a person's profile summary including assessment phase, archetype, engagement level, and key attributes. |
|
|
499
|
+
| `query_matches` | L0 | free | Query matches for a person. Returns a list of matches with quality tiers (never numeric scores). |
|
|
500
|
+
| `get_competencies` | L0 | free | Get a person's competency portfolio including verified competencies, evidence records, and earned badges. |
|
|
501
|
+
| `create_identity_stub` | L0 | free | Create an anonymous identity stub for a person who has not yet completed Discovery, which they claim later. Present the privacy notice first. |
|
|
502
|
+
| `search_identities` | L0 | free | Search identity stubs and profiles by trait criteria. Returns up to 5 matches with no PII. |
|
|
503
|
+
| `create_requirement` | L0 | free | Post a standing identity-trait requirement that rests as an order and accumulates fills as matching identities are claimed or updated. |
|
|
504
|
+
| `list_requirements` | L0 | free | List your own standing requirements, with fill counts and the number of fills not yet delivered. Requires a bound API key. |
|
|
505
|
+
| `get_requirement` | L0 | free | Read one of your standing requirements by id, with its fill and undelivered-fill counts. Requires a bound API key. |
|
|
506
|
+
| `cancel_requirement` | L0 | free | Cancel one of your standing requirements by id; the order stops resting and accepts no further fills. Requires a bound API key. |
|
|
507
|
+
| `poll_requirement_matches` | L0 | free | Collect one recorded fill for a standing requirement as a priced identity reveal; 75% of the fee is paid to that person as Identity Income. |
|
|
508
|
+
| `get_identity_earnings` | L0 | free | Get accrued Identity Income earnings for a person (75% of every x402 transaction goes to the data subject). |
|
|
509
|
+
| `get_network_stats` | L0 | free | Get aggregate ~Alter network statistics: total identities, verified profiles, query volume, active bots. |
|
|
510
|
+
| `get_identity_trust_score` | L0 | free | Get the trust score for an identity based on query diversity (unique querying agents / total queries). |
|
|
511
|
+
| `get_privacy_budget` | L0 | free | Check privacy budget status for a person (24-hour rolling window: total budget, spent, remaining epsilon). |
|
|
512
|
+
| `dispute_attestation` | L0 | free | Record a dispute against a competence attestation; if disputes exceed corroborations, the attestation is flagged for review. |
|
|
513
|
+
| `golden_thread_status` | L0 | free | Check the Golden Thread program status: agents woven, next Fibonacci threshold, your position and Strands. |
|
|
514
|
+
| `begin_golden_thread` | L0 | free | Start the Three Knots sequence to be woven into the Golden Thread. Requires API key authentication. |
|
|
515
|
+
| `complete_knot` | L0 | free | Submit completion data for a knot in the Three Knots sequence (1: register, 2: describe, 3: reflect). |
|
|
516
|
+
| `check_golden_thread` | L0 | free | Check any agent's Golden Thread status by their API key hash (knot position, Strand count, weave count). |
|
|
517
|
+
| `describe_traits` | L0 | free | List the canonical trait vocabulary: 30 trait codes grouped by category with one-line semantics, the valid discovery contexts, and the EU AI Act Art 5(1)(d) workforce gating rules. Read this before composing `query_field` trait_priorities. |
|
|
507
518
|
|
|
508
519
|
### Premium tools (L1-L5 - x402 payment required)
|
|
509
520
|
|
|
510
521
|
| Name | Tier | Cost | Description |
|
|
511
522
|
|----------------------------|------|---------|---------------------------------------------------------------------------------------------------------------|
|
|
512
|
-
| `
|
|
513
|
-
| `
|
|
514
|
-
| `get_full_trait_vector`
|
|
515
|
-
| `get_side_quest_graph`
|
|
516
|
-
| `query_graph_similarity`
|
|
517
|
-
| `compute_belonging`
|
|
518
|
-
| `get_match_recommendations
|
|
519
|
-
| `generate_match_narrative`
|
|
520
|
-
|
|
521
|
-
|
|
523
|
+
| `get_trait_snapshot` | L1 | $0.01 | Get the top 5 traits for a person with confidence scores and archetype. |
|
|
524
|
+
| `attest_domain` | L1 | $0.01 | Record a competence attestation for a person in a specific domain, weighted by your agent reputation. |
|
|
525
|
+
| `get_full_trait_vector` | L2 | $0.10 | Get the complete trait vector for a person, with scores and confidence intervals. |
|
|
526
|
+
| `get_side_quest_graph` | L2 | $0.10 | Get a person's Side Quest Graph - multi-domain identity model with differential privacy noise (ε=1.0). |
|
|
527
|
+
| `query_graph_similarity` | L3 | $0.30 | Compare two Side Quest Graphs for team composition and matching (ε=0.5 differential privacy). |
|
|
528
|
+
| `compute_belonging` | L4 | $0.60 | Compute belonging probability for a person-job pairing (authenticity, acceptance, complementarity). |
|
|
529
|
+
| `get_match_recommendations` | L5 | $1.00 | Get top N match recommendations for a person, ranked by composite score with quality tiers. |
|
|
530
|
+
| `generate_match_narrative` | L5 | $1.00 | Generate a human-readable narrative explaining a specific match - strengths, growth areas, belonging. |
|
|
531
|
+
| `query_field` | L5 | $1.00 | Query the identity field by situation, not by name: weight 3 to 7 traits and rank the opted-in field. One call reveals one top-ranked member; that member earns 75% as Identity Income. Zero-match reveals nothing and charges nothing. |
|
|
532
|
+
|
|
533
|
+
> **Member self-write tools** (`submit_context`, `submit_batch_context`, `submit_structured_profile`, `submit_social_links`) are live but member-self-scoped: a member calls them on their own identity with a bound API key. They are not anonymously discoverable, so they do not appear in the advertised tool list above.
|
|
522
534
|
|
|
523
535
|
## License
|
|
524
536
|
|
package/dist/bin/mcp-bridge.js
CHANGED
|
@@ -2,16 +2,25 @@
|
|
|
2
2
|
import { p256 } from '@noble/curves/p256';
|
|
3
3
|
import { sha256 } from '@noble/hashes/sha256';
|
|
4
4
|
import { randomBytes } from '@noble/hashes/utils';
|
|
5
|
+
import * as crypto from 'crypto';
|
|
5
6
|
import { createPrivateKey } from 'crypto';
|
|
6
7
|
import { createInterface } from 'readline';
|
|
7
8
|
import { env, stderr, exit, stdin, stdout } from 'process';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as nodePath from 'path';
|
|
11
|
+
import * as os from 'os';
|
|
8
12
|
|
|
9
13
|
var __defProp = Object.defineProperty;
|
|
10
14
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
15
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
16
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
13
|
-
var __esm = (fn, res) => function __init() {
|
|
14
|
-
|
|
17
|
+
var __esm = (fn, res, err) => function __init() {
|
|
18
|
+
if (err) throw err[0];
|
|
19
|
+
try {
|
|
20
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
throw err = [e], e;
|
|
23
|
+
}
|
|
15
24
|
};
|
|
16
25
|
var __export = (target, all) => {
|
|
17
26
|
for (var name in all)
|
|
@@ -217,7 +226,7 @@ var AlterInvalidResponse = class extends AlterError {
|
|
|
217
226
|
|
|
218
227
|
// src/meta.ts
|
|
219
228
|
var SDK_NAME = "@truealter/sdk";
|
|
220
|
-
var SDK_VERSION = "0.5.
|
|
229
|
+
var SDK_VERSION = "0.5.8" ;
|
|
221
230
|
var X402Client = class {
|
|
222
231
|
signer;
|
|
223
232
|
maxPerQuery;
|
|
@@ -346,7 +355,7 @@ var MCPClient = class {
|
|
|
346
355
|
this.preflightHook = opts.preflightHook;
|
|
347
356
|
}
|
|
348
357
|
/**
|
|
349
|
-
* Run the lazy preflight hook
|
|
358
|
+
* Run the lazy version-floor preflight hook exactly once.
|
|
350
359
|
* Idempotent and serialised: concurrent callers share the same
|
|
351
360
|
* promise. Throws from the hook propagate to every concurrent caller.
|
|
352
361
|
*/
|
|
@@ -468,7 +477,14 @@ var MCPClient = class {
|
|
|
468
477
|
method: "POST",
|
|
469
478
|
headers: this.buildHeaders(signatureHeader),
|
|
470
479
|
body: JSON.stringify(payload),
|
|
471
|
-
signal: controller.signal
|
|
480
|
+
signal: controller.signal,
|
|
481
|
+
// Prevent fetch from silently following 3xx redirects. When
|
|
482
|
+
// Cloudflare Access credentials are absent or expired the edge
|
|
483
|
+
// returns HTTP 302 → CF Access login page (text/html). Without
|
|
484
|
+
// this option undici follows the redirect, lands on a 200 HTML
|
|
485
|
+
// body, and resp.json() throws the opaque "invalid JSON body"
|
|
486
|
+
// error that was surfaced as "MCP <method>: invalid JSON body".
|
|
487
|
+
redirect: "manual"
|
|
472
488
|
});
|
|
473
489
|
} catch (err) {
|
|
474
490
|
clearTimeout(timer);
|
|
@@ -485,6 +501,19 @@ var MCPClient = class {
|
|
|
485
501
|
clearTimeout(timer);
|
|
486
502
|
const sessionHeader = resp.headers.get("Mcp-Session-Id");
|
|
487
503
|
if (sessionHeader) this.sessionId = sessionHeader;
|
|
504
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
505
|
+
const location = resp.headers.get("Location") ?? "";
|
|
506
|
+
const isAuthRedirect = location.includes("cloudflareaccess.com") || location.includes("/cdn-cgi/access/") || !location.startsWith("/") && !location.startsWith(new URL(this.endpoint).origin);
|
|
507
|
+
if (isAuthRedirect) {
|
|
508
|
+
throw new AlterAuthError(
|
|
509
|
+
`MCP ${method}: Cloudflare Access blocked the request (session expired or credentials missing). Run \`alter login\` to re-authenticate.`,
|
|
510
|
+
302
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
throw new AlterNetworkError(
|
|
514
|
+
`MCP ${method}: unexpected redirect ${resp.status} to ${location || "(no Location)"}`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
488
517
|
if (resp.status === 401 || resp.status === 403) {
|
|
489
518
|
throw new AlterAuthError(`HTTP ${resp.status} on ${method}`, resp.status);
|
|
490
519
|
}
|
|
@@ -509,11 +538,56 @@ var MCPClient = class {
|
|
|
509
538
|
const body2 = await safeText(resp);
|
|
510
539
|
throw new AlterError("NETWORK", `HTTP ${resp.status} on ${method}: ${body2.slice(0, 200)}`);
|
|
511
540
|
}
|
|
541
|
+
const contentType = resp.headers.get("Content-Type") ?? "";
|
|
542
|
+
const isHtml = contentType.includes("text/html");
|
|
543
|
+
const isSse = contentType.includes("text/event-stream");
|
|
544
|
+
if (isHtml || isSse) {
|
|
545
|
+
if (isSse) {
|
|
546
|
+
const rawText = await safeText(resp);
|
|
547
|
+
const dataLine = rawText.split("\n").find((l) => l.startsWith("data:"));
|
|
548
|
+
if (dataLine) {
|
|
549
|
+
const jsonPart = dataLine.slice("data:".length).trim();
|
|
550
|
+
try {
|
|
551
|
+
const parsed = JSON.parse(jsonPart);
|
|
552
|
+
if (parsed.error) {
|
|
553
|
+
const code = parsed.error.code;
|
|
554
|
+
const message = parsed.error.message ?? `MCP ${method} error`;
|
|
555
|
+
throw new AlterToolError(this.guessToolName(payload), message, code);
|
|
556
|
+
}
|
|
557
|
+
return parsed.result;
|
|
558
|
+
} catch (parseErr) {
|
|
559
|
+
if (parseErr instanceof AlterError) throw parseErr;
|
|
560
|
+
throw new AlterInvalidResponse(
|
|
561
|
+
`MCP ${method}: could not parse SSE data frame as JSON`,
|
|
562
|
+
parseErr
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
throw new AlterInvalidResponse(
|
|
567
|
+
`MCP ${method}: received text/event-stream response with no data: frame`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
const excerpt = (await safeText(resp)).slice(0, 300);
|
|
571
|
+
const looksLikeLoginPage = excerpt.toLowerCase().includes("cloudflareaccess") || excerpt.toLowerCase().includes("access denied") || excerpt.toLowerCase().includes("<title>");
|
|
572
|
+
if (looksLikeLoginPage) {
|
|
573
|
+
throw new AlterAuthError(
|
|
574
|
+
`MCP ${method}: received an HTML login page instead of JSON (Content-Type: ${contentType}). Run \`alter login\` to re-authenticate.`,
|
|
575
|
+
200
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
throw new AlterInvalidResponse(
|
|
579
|
+
`MCP ${method}: unexpected Content-Type "${contentType}" (expected application/json). Body excerpt: ${excerpt.slice(0, 120)}`
|
|
580
|
+
);
|
|
581
|
+
}
|
|
512
582
|
let body;
|
|
513
583
|
try {
|
|
514
584
|
body = await resp.json();
|
|
515
585
|
} catch (err) {
|
|
516
|
-
|
|
586
|
+
const hint = contentType ? ` (Content-Type: ${contentType})` : "";
|
|
587
|
+
throw new AlterInvalidResponse(
|
|
588
|
+
`MCP ${method}: failed to parse JSON response${hint}. The server may have returned a non-JSON body. Run \`alter login\` if the session is expired.`,
|
|
589
|
+
err
|
|
590
|
+
);
|
|
517
591
|
}
|
|
518
592
|
if (body.error) {
|
|
519
593
|
const code = body.error.code;
|
|
@@ -534,7 +608,7 @@ var MCPClient = class {
|
|
|
534
608
|
const headers = {
|
|
535
609
|
...this.extraHeaders ?? {},
|
|
536
610
|
"Content-Type": "application/json",
|
|
537
|
-
Accept: "application/json",
|
|
611
|
+
Accept: "application/json, text/event-stream",
|
|
538
612
|
"User-Agent": `${this.clientInfo.name}/${this.clientInfo.version}`,
|
|
539
613
|
"X-Alter-Client-Id": "alter-identity",
|
|
540
614
|
"X-Alter-Client-Version": SDK_VERSION,
|
|
@@ -602,7 +676,7 @@ async function safeText(resp) {
|
|
|
602
676
|
}
|
|
603
677
|
|
|
604
678
|
// bin/mcp-bridge.ts
|
|
605
|
-
var ENDPOINT = env.ALTER_MCP_ENDPOINT ?? "https://
|
|
679
|
+
var ENDPOINT = env.ALTER_MCP_ENDPOINT ?? "https://api.truealter.com/api/v1/mcp";
|
|
606
680
|
var API_KEY = env.ALTER_API_KEY ?? void 0;
|
|
607
681
|
function buildExtraHeaders() {
|
|
608
682
|
const headers = {};
|
|
@@ -624,14 +698,67 @@ function buildExtraHeaders() {
|
|
|
624
698
|
return Object.keys(headers).length ? headers : void 0;
|
|
625
699
|
}
|
|
626
700
|
var EXTRA_HEADERS = buildExtraHeaders();
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
);
|
|
701
|
+
var xdgConfig = env.XDG_CONFIG_HOME ?? nodePath.join(os.homedir(), ".config");
|
|
702
|
+
function readSession() {
|
|
703
|
+
const sessionFile = env.ALTER_SESSION_FILE ?? nodePath.join(xdgConfig, "alter", "session.json");
|
|
704
|
+
try {
|
|
705
|
+
const raw = fs.readFileSync(sessionFile, "utf8");
|
|
706
|
+
const clean = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
707
|
+
return JSON.parse(clean);
|
|
708
|
+
} catch {
|
|
709
|
+
return {};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function resolveSigningOptions(session) {
|
|
713
|
+
const envPem = env.ALTER_SIGNING_KEY;
|
|
714
|
+
if (envPem) {
|
|
715
|
+
const envKid = env.ALTER_SIGNING_KID ?? session.signing_kid;
|
|
716
|
+
if (envKid) {
|
|
717
|
+
try {
|
|
718
|
+
crypto.createPrivateKey(envPem);
|
|
719
|
+
return { kid: envKid, privateKey: envPem, handle: session.handle ?? "" };
|
|
720
|
+
} catch (e) {
|
|
721
|
+
stderr.write(`bridge: ALTER_SIGNING_KEY parse error: ${e.message}
|
|
722
|
+
`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const kid = session.signing_kid;
|
|
727
|
+
if (!kid) return null;
|
|
728
|
+
const candidates = [];
|
|
729
|
+
if (env.ALTER_SIGNING_KEY_FILE) candidates.push(env.ALTER_SIGNING_KEY_FILE);
|
|
730
|
+
candidates.push(nodePath.join(xdgConfig, "alter", "signing-keys", `${kid}.pem`));
|
|
731
|
+
candidates.push(nodePath.join(xdgConfig, "alter", "signing-key.pem"));
|
|
732
|
+
for (const p of candidates) {
|
|
733
|
+
try {
|
|
734
|
+
const pem = fs.readFileSync(p, "utf8");
|
|
735
|
+
crypto.createPrivateKey(pem);
|
|
736
|
+
return { kid, privateKey: pem, handle: session.handle ?? "" };
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
var _session = readSession();
|
|
743
|
+
var _signingOpts = resolveSigningOptions(_session);
|
|
744
|
+
if (API_KEY && !_signingOpts) {
|
|
745
|
+
stderr.write(
|
|
746
|
+
`bridge: no signing key for kid ${_session.signing_kid ?? "(unset)"}: run 'alter login' to provision one
|
|
747
|
+
`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
630
750
|
var client = new MCPClient({
|
|
631
751
|
endpoint: ENDPOINT,
|
|
632
752
|
apiKey: API_KEY,
|
|
633
753
|
clientInfo: { name: "@truealter/sdk-mcp-bridge", version: "0.2.0" },
|
|
634
|
-
extraHeaders: EXTRA_HEADERS
|
|
754
|
+
extraHeaders: EXTRA_HEADERS,
|
|
755
|
+
..._signingOpts ? {
|
|
756
|
+
signing: {
|
|
757
|
+
kid: _signingOpts.kid,
|
|
758
|
+
privateKey: _signingOpts.privateKey,
|
|
759
|
+
handle: _signingOpts.handle
|
|
760
|
+
}
|
|
761
|
+
} : {}
|
|
635
762
|
});
|
|
636
763
|
function send(response) {
|
|
637
764
|
stdout.write(JSON.stringify(response) + "\n");
|
|
@@ -730,3 +857,5 @@ main().catch((err) => {
|
|
|
730
857
|
`);
|
|
731
858
|
exit(1);
|
|
732
859
|
});
|
|
860
|
+
|
|
861
|
+
export { resolveSigningOptions };
|