@absolutejs/mcp 0.17.2 → 0.17.4
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/CHANGELOG.md +13 -0
- package/canary/authenticated.ts +192 -0
- package/canary/results/2026-09-11-vscode.json +28 -0
- package/canary/server.ts +1 -1
- package/changelog.json +24 -0
- package/dist/index.js +23 -7
- package/docs/host-canaries.md +28 -3
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,19 @@ This file is generated by `absolute-changelog` from the entries in
|
|
|
6
6
|
`changelog/`. Edit an entry, not this file — and add new ones under
|
|
7
7
|
`changelog/unreleased/`.
|
|
8
8
|
|
|
9
|
+
## 0.17.4 — 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Ship an authenticated two-account staging canary for read isolation, credential rejection and session reconnect with private credentials and aggregate-only results**
|
|
14
|
+
|
|
15
|
+
## 0.17.3 — 2026-09-11
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **Authorize session deletion, recover explicit client initialization after expiry, and preserve typed HTTP failures without replaying tools**
|
|
20
|
+
- **Accept the standard protocol-header compatibility fallback used by VS Code Copilot and record native report rendering evidence**
|
|
21
|
+
|
|
9
22
|
## 0.17.2 — 2026-09-11
|
|
10
23
|
|
|
11
24
|
### Added
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/** Authenticated staging checks. Tokens/results remain in memory and are never logged. */
|
|
2
|
+
import type { McpToolResult } from "@absolutejs/mcp";
|
|
3
|
+
export type AuthenticatedCanaryOptions = {
|
|
4
|
+
url: string;
|
|
5
|
+
accounts: readonly [string, string];
|
|
6
|
+
tool: { name: string; arguments?: Record<string, unknown> };
|
|
7
|
+
/** Select stable account-owned data, excluding timestamps and shared metadata. */
|
|
8
|
+
fingerprint: (result: McpToolResult) => string;
|
|
9
|
+
request?: typeof fetch;
|
|
10
|
+
};
|
|
11
|
+
export const runAuthenticatedCanary = async (
|
|
12
|
+
options: AuthenticatedCanaryOptions,
|
|
13
|
+
) => {
|
|
14
|
+
const url = new URL(options.url);
|
|
15
|
+
if (
|
|
16
|
+
url.protocol !== "https:" &&
|
|
17
|
+
!(
|
|
18
|
+
url.protocol === "http:" &&
|
|
19
|
+
["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
throw Error("HTTPS required outside loopback");
|
|
23
|
+
if (
|
|
24
|
+
!options.accounts[0] ||
|
|
25
|
+
!options.accounts[1] ||
|
|
26
|
+
options.accounts[0] === options.accounts[1]
|
|
27
|
+
)
|
|
28
|
+
throw Error("Two distinct credentials required");
|
|
29
|
+
const request = options.request ?? fetch;
|
|
30
|
+
const checks: { name: string; passed: boolean }[] = [];
|
|
31
|
+
const sessions: { token: string; id: string; protocol: string }[] = [];
|
|
32
|
+
const send = async (
|
|
33
|
+
token: string | null,
|
|
34
|
+
session: (typeof sessions)[number] | undefined,
|
|
35
|
+
method: string,
|
|
36
|
+
params: unknown = {},
|
|
37
|
+
) =>
|
|
38
|
+
request(options.url, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
redirect: "error",
|
|
41
|
+
signal: AbortSignal.timeout(15000),
|
|
42
|
+
headers: {
|
|
43
|
+
"content-type": "application/json",
|
|
44
|
+
accept: "application/json, text/event-stream",
|
|
45
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
46
|
+
...(session
|
|
47
|
+
? {
|
|
48
|
+
"mcp-session-id": session.id,
|
|
49
|
+
"mcp-protocol-version": session.protocol,
|
|
50
|
+
}
|
|
51
|
+
: {}),
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
54
|
+
});
|
|
55
|
+
const body = async (response: Response) => {
|
|
56
|
+
if (
|
|
57
|
+
!response.ok ||
|
|
58
|
+
!response.headers.get("content-type")?.includes("application/json")
|
|
59
|
+
)
|
|
60
|
+
throw Error("Canary requires successful JSON responses");
|
|
61
|
+
const value = await response.json();
|
|
62
|
+
if (value.error || !value.result || value.result.isError)
|
|
63
|
+
throw Error("Canary RPC failed");
|
|
64
|
+
return value.result;
|
|
65
|
+
};
|
|
66
|
+
const initialize = async (token: string) => {
|
|
67
|
+
const response = await send(token, undefined, "initialize", {
|
|
68
|
+
protocolVersion: "2025-11-25",
|
|
69
|
+
clientInfo: { name: "absolute-authenticated-canary", version: "1" },
|
|
70
|
+
capabilities: {
|
|
71
|
+
elicitation: {},
|
|
72
|
+
extensions: {
|
|
73
|
+
"io.modelcontextprotocol/ui": {
|
|
74
|
+
mimeTypes: ["text/html;profile=mcp-app"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
const result = await body(response);
|
|
80
|
+
const id = response.headers.get("mcp-session-id");
|
|
81
|
+
if (!id || typeof result.protocolVersion !== "string")
|
|
82
|
+
throw Error("Stateful MCP session required");
|
|
83
|
+
const session = { token, id, protocol: result.protocolVersion };
|
|
84
|
+
sessions.push(session);
|
|
85
|
+
await request(options.url, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
redirect: "error",
|
|
88
|
+
signal: AbortSignal.timeout(15000),
|
|
89
|
+
headers: {
|
|
90
|
+
authorization: `Bearer ${token}`,
|
|
91
|
+
"content-type": "application/json",
|
|
92
|
+
"mcp-session-id": id,
|
|
93
|
+
"mcp-protocol-version": session.protocol,
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
jsonrpc: "2.0",
|
|
97
|
+
method: "notifications/initialized",
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
return session;
|
|
101
|
+
};
|
|
102
|
+
const drop = (session: (typeof sessions)[number], authorized = true) =>
|
|
103
|
+
request(options.url, {
|
|
104
|
+
method: "DELETE",
|
|
105
|
+
redirect: "error",
|
|
106
|
+
signal: AbortSignal.timeout(15000),
|
|
107
|
+
headers: {
|
|
108
|
+
"mcp-session-id": session.id,
|
|
109
|
+
"mcp-protocol-version": session.protocol,
|
|
110
|
+
...(authorized ? { authorization: `Bearer ${session.token}` } : {}),
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const read = async (token: string, session: (typeof sessions)[number]) =>
|
|
114
|
+
options.fingerprint(
|
|
115
|
+
await body(await send(token, session, "tools/call", options.tool)),
|
|
116
|
+
);
|
|
117
|
+
try {
|
|
118
|
+
const a = await initialize(options.accounts[0]);
|
|
119
|
+
const b = await initialize(options.accounts[1]);
|
|
120
|
+
for (const session of [a, b]) {
|
|
121
|
+
const discovery = await body(
|
|
122
|
+
await send(session.token, session, "tools/list"),
|
|
123
|
+
);
|
|
124
|
+
if (
|
|
125
|
+
!discovery.tools?.some(
|
|
126
|
+
(tool: { name: string; annotations?: { readOnlyHint?: boolean } }) =>
|
|
127
|
+
tool.name === options.tool.name &&
|
|
128
|
+
tool.annotations?.readOnlyHint === true,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
throw Error("Explicitly read-only tool required for both accounts");
|
|
132
|
+
}
|
|
133
|
+
const first = await read(a.token, a);
|
|
134
|
+
const second = await read(b.token, b);
|
|
135
|
+
if (!first || !second || first === second)
|
|
136
|
+
throw Error(
|
|
137
|
+
"Distinct stable account-owned results required; isolation is inconclusive",
|
|
138
|
+
);
|
|
139
|
+
checks.push({ name: "distinct-account-results", passed: true });
|
|
140
|
+
for (const [session, expected] of [
|
|
141
|
+
[a, first],
|
|
142
|
+
[b, second],
|
|
143
|
+
] as const)
|
|
144
|
+
checks.push({
|
|
145
|
+
name: "stable-account-result",
|
|
146
|
+
passed: (await read(session.token, session)) === expected,
|
|
147
|
+
});
|
|
148
|
+
for (const [token, session, expected] of [
|
|
149
|
+
[b.token, a, second],
|
|
150
|
+
[a.token, b, first],
|
|
151
|
+
] as const) {
|
|
152
|
+
const response = await send(token, session, "tools/call", options.tool);
|
|
153
|
+
// Binding a session to its owner is also safe; otherwise current credentials must select the data.
|
|
154
|
+
checks.push({
|
|
155
|
+
name: "cross-session-account-isolation",
|
|
156
|
+
passed:
|
|
157
|
+
[401, 403, 404].includes(response.status) ||
|
|
158
|
+
options.fingerprint(await body(response)) === expected,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
for (const token of [null, "invalid-canary-credential"])
|
|
162
|
+
checks.push({
|
|
163
|
+
name: "invalid-credential-rejected",
|
|
164
|
+
passed: (await send(token, a, "tools/list")).status === 401,
|
|
165
|
+
});
|
|
166
|
+
checks.push({
|
|
167
|
+
name: "unauthenticated-delete-rejected",
|
|
168
|
+
passed: (await drop(a, false)).status === 401,
|
|
169
|
+
});
|
|
170
|
+
checks.push({
|
|
171
|
+
name: "session-survives-rejected-delete",
|
|
172
|
+
passed: (await send(a.token, a, "tools/list")).status === 200,
|
|
173
|
+
});
|
|
174
|
+
const deletion = await drop(b);
|
|
175
|
+
checks.push({
|
|
176
|
+
name: "own-session-deletion",
|
|
177
|
+
passed: deletion.status === 204,
|
|
178
|
+
});
|
|
179
|
+
checks.push({
|
|
180
|
+
name: "terminated-session-404",
|
|
181
|
+
passed: (await send(b.token, b, "tools/list")).status === 404,
|
|
182
|
+
});
|
|
183
|
+
const fresh = await initialize(b.token);
|
|
184
|
+
checks.push({
|
|
185
|
+
name: "fresh-session-read",
|
|
186
|
+
passed: fresh.id !== b.id && (await read(b.token, fresh)) === second,
|
|
187
|
+
});
|
|
188
|
+
return { passed: checks.every((check) => check.passed), checks };
|
|
189
|
+
} finally {
|
|
190
|
+
for (const session of sessions) await drop(session).catch(() => undefined);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"date": "2026-09-11",
|
|
3
|
+
"runtimePackage": "@absolutejs/mcp@0.17.3 pre-release build",
|
|
4
|
+
"host": "Visual Studio Code 1.135.0, native Windows Copilot Chat",
|
|
5
|
+
"transport": "Streamable HTTP, isolated loopback synthetic workspace",
|
|
6
|
+
"protocol": "2025-11-25",
|
|
7
|
+
"protocolHeader": null,
|
|
8
|
+
"appsMimeType": "text/html;profile=mcp-app",
|
|
9
|
+
"successfulCalls": {
|
|
10
|
+
"get_billing_status": 2,
|
|
11
|
+
"get_usage_report": 2,
|
|
12
|
+
"list_receipts": 2
|
|
13
|
+
},
|
|
14
|
+
"observed": [
|
|
15
|
+
"All three native report views rendered after window reload",
|
|
16
|
+
"Balance and usage refresh succeeded",
|
|
17
|
+
"Daily usage expanded",
|
|
18
|
+
"Receipt pagination reached empty final page and hid Older receipts",
|
|
19
|
+
"Initial render and window reload made no extra report calls",
|
|
20
|
+
"All three app documents measured 242px with no horizontal document overflow"
|
|
21
|
+
],
|
|
22
|
+
"limitations": [
|
|
23
|
+
"Initial balance webview was blank; window reload restored it; cause not isolated",
|
|
24
|
+
"Window reload restores original tool results, including first receipt page",
|
|
25
|
+
"OAuth, real-account isolation and expired-session reconnect against authenticated staging not tested",
|
|
26
|
+
"Host commerce permissions not certified"
|
|
27
|
+
]
|
|
28
|
+
}
|
package/canary/server.ts
CHANGED
|
@@ -49,7 +49,6 @@ const handler = createMcpHandler({
|
|
|
49
49
|
issuer: "http://127.0.0.1:4428",
|
|
50
50
|
path: "/mcp",
|
|
51
51
|
serverInfo: { name: "absolute-billing-canary", version: "1" },
|
|
52
|
-
supportedProtocols: ["2025-06-18"],
|
|
53
52
|
authorize: async () => ({
|
|
54
53
|
ok: true,
|
|
55
54
|
caller: "synthetic-fixture",
|
|
@@ -110,6 +109,7 @@ Bun.serve({
|
|
|
110
109
|
JSON.stringify({
|
|
111
110
|
method: rpc?.method ?? request.method,
|
|
112
111
|
status: response.status,
|
|
112
|
+
protocolHeader: request.headers.get("mcp-protocol-version"),
|
|
113
113
|
...(rpc?.method === "initialize"
|
|
114
114
|
? {
|
|
115
115
|
client: rpc.params?.clientInfo?.name,
|
package/changelog.json
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
"contract": 1,
|
|
3
3
|
"name": "@absolutejs/mcp",
|
|
4
4
|
"releases": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.17.4",
|
|
7
|
+
"date": "2026-09-11",
|
|
8
|
+
"changes": [
|
|
9
|
+
{
|
|
10
|
+
"kind": "added",
|
|
11
|
+
"summary": "Ship an authenticated two-account staging canary for read isolation, credential rejection and session reconnect with private credentials and aggregate-only results"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.17.3",
|
|
17
|
+
"date": "2026-09-11",
|
|
18
|
+
"changes": [
|
|
19
|
+
{
|
|
20
|
+
"kind": "fixed",
|
|
21
|
+
"summary": "Authorize session deletion, recover explicit client initialization after expiry, and preserve typed HTTP failures without replaying tools"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"kind": "fixed",
|
|
25
|
+
"summary": "Accept the standard protocol-header compatibility fallback used by VS Code Copilot and record native report rendering evidence"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
5
29
|
{
|
|
6
30
|
"version": "0.17.2",
|
|
7
31
|
"date": "2026-09-11",
|
package/dist/index.js
CHANGED
|
@@ -735,14 +735,25 @@ var createMcpClient = (options) => {
|
|
|
735
735
|
});
|
|
736
736
|
}
|
|
737
737
|
}
|
|
738
|
-
const captured = response.headers.get("mcp-session-id");
|
|
739
|
-
if (captured)
|
|
740
|
-
sessionId = captured;
|
|
741
738
|
if (response.status === 401) {
|
|
742
739
|
throw new McpClientError("The MCP server rejected the credentials", {
|
|
743
740
|
status: 401
|
|
744
741
|
});
|
|
745
742
|
}
|
|
743
|
+
if (!response.ok) {
|
|
744
|
+
if (response.status === 404 && sessionId === headers["mcp-session-id"]) {
|
|
745
|
+
sessionId = null;
|
|
746
|
+
}
|
|
747
|
+
const failure = await parseBody(response, maxBytes).catch(() => null);
|
|
748
|
+
const error = isRecord(failure) && isRecord(failure.error) ? failure.error : null;
|
|
749
|
+
throw new McpClientError(typeof error?.message === "string" ? error.message : `MCP HTTP request failed (${response.status})`, {
|
|
750
|
+
status: response.status,
|
|
751
|
+
code: typeof error?.code === "number" ? error.code : undefined
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
const captured = response.headers.get("mcp-session-id");
|
|
755
|
+
if (captured)
|
|
756
|
+
sessionId = captured;
|
|
746
757
|
const isStream = (response.headers.get("content-type") ?? "").includes("text/event-stream");
|
|
747
758
|
const payload = isStream ? await consumeSseStream(response, maxBytes, answerServer) : await parseBody(response, maxBytes);
|
|
748
759
|
if (!isRecord(payload)) {
|
|
@@ -776,6 +787,7 @@ var createMcpClient = (options) => {
|
|
|
776
787
|
});
|
|
777
788
|
};
|
|
778
789
|
const initialize = async () => {
|
|
790
|
+
sessionId = null;
|
|
779
791
|
const result2 = await rpc("initialize", {
|
|
780
792
|
capabilities: {
|
|
781
793
|
...options.onElicit ? { elicitation: { form: {}, url: {} } } : {},
|
|
@@ -2197,15 +2209,15 @@ var runMcpPost = async (config, request, body) => {
|
|
|
2197
2209
|
return rpcError(null, JSONRPC_INVALID_REQUEST, "Batching is not supported");
|
|
2198
2210
|
}
|
|
2199
2211
|
const isInitialize = typeof body === "object" && body !== null && "method" in body && body.method === "initialize";
|
|
2200
|
-
const protocolVersion = request.headers.get("mcp-protocol-version");
|
|
2212
|
+
const protocolVersion = request.headers.get("mcp-protocol-version") ?? "2025-03-26";
|
|
2201
2213
|
const supportedProtocols = config.supportedProtocols ?? [
|
|
2202
2214
|
MCP_LATEST_PROTOCOL_VERSION,
|
|
2203
2215
|
"2025-06-18",
|
|
2204
2216
|
"2025-03-26",
|
|
2205
2217
|
"2024-11-05"
|
|
2206
2218
|
];
|
|
2207
|
-
if (!isInitialize &&
|
|
2208
|
-
return new Response("
|
|
2219
|
+
if (!isInitialize && !supportedProtocols.includes(protocolVersion)) {
|
|
2220
|
+
return new Response("Unsupported MCP-Protocol-Version", {
|
|
2209
2221
|
status: 400
|
|
2210
2222
|
});
|
|
2211
2223
|
}
|
|
@@ -2215,13 +2227,17 @@ var runMcpPost = async (config, request, body) => {
|
|
|
2215
2227
|
return new Response(null, { status: HTTP_NOT_FOUND });
|
|
2216
2228
|
}
|
|
2217
2229
|
return dispatchMcp(config, auth.caller, auth.scopes ?? [], body, {
|
|
2218
|
-
protocolVersion
|
|
2230
|
+
protocolVersion,
|
|
2219
2231
|
requestSignal: request.signal,
|
|
2220
2232
|
sessionId,
|
|
2221
2233
|
sessions
|
|
2222
2234
|
}).catch(() => rpcError(null, JSONRPC_INVALID_REQUEST, "Internal error"));
|
|
2223
2235
|
};
|
|
2224
2236
|
var runMcpDelete = async (config, request) => {
|
|
2237
|
+
const auth = await config.authorize(request);
|
|
2238
|
+
if (!auth.ok) {
|
|
2239
|
+
return unauthorized(`${config.issuer}${metadataPathFor(config.path)}`, auth.reason);
|
|
2240
|
+
}
|
|
2225
2241
|
const sessions = registryFor(config);
|
|
2226
2242
|
const sessionId = request.headers.get("mcp-session-id");
|
|
2227
2243
|
if (!sessions || !sessionId) {
|
package/docs/host-canaries.md
CHANGED
|
@@ -4,7 +4,7 @@ The package ships `canary/server.ts`: a loopback-only, synthetic billing endpoin
|
|
|
4
4
|
|
|
5
5
|
From an installed package, run `bun node_modules/@absolutejs/mcp/canary/server.ts`. From this repository, build first, then `bun run canary`. The fixed endpoint is `http://127.0.0.1:4428/mcp`. Startup validates all four report calls before listening. Stop it with Ctrl-C after the test. A port conflict fails startup rather than replacing another service.
|
|
6
6
|
|
|
7
|
-
The JSONL log records protocol methods, status, client name/version, negotiated protocol, advertised Apps MIME types and tool success. It omits headers, session IDs, tool arguments and report bodies. Keep host transcripts local: host logs can contain unrelated account or environment information. Commit only reviewed evidence.
|
|
7
|
+
The JSONL log records protocol methods, status, client name/version, negotiated protocol, advertised Apps MIME types and tool success. It records only the MCP protocol-version header and omits all other headers, session IDs, tool arguments and report bodies. Keep host transcripts local: host logs can contain unrelated account or environment information. Commit only reviewed evidence.
|
|
8
8
|
|
|
9
9
|
## Host connections
|
|
10
10
|
|
|
@@ -49,7 +49,7 @@ Terminal tests ran against the built `0.17.0` runtime. The later Claude web test
|
|
|
49
49
|
| Claude Code 2.1.265, print mode, Streamable HTTP | Requested 2025-11-25; accepted server 2025-06-18; no Apps MIME extension | Four read calls passed, including cursor pagination; text/structured fallback; no embedded rendering claim |
|
|
50
50
|
| Codex CLI 0.154.0, exec mode, Streamable HTTP | Requested and accepted 2025-06-18; no Apps MIME extension | Four read calls passed, including cursor pagination; text/structured fallback; no embedded rendering claim |
|
|
51
51
|
| Claude web, Windows Chrome 152.0.7977.83 | Apps MIME advertised; accepted 2025-06-18 | Three native views, refresh and pagination passed |
|
|
52
|
-
| VS Code 1.135.0 |
|
|
52
|
+
| VS Code 1.135.0 | Apps MIME advertised; negotiated 2025-11-25; omitted protocol header | Three views, refresh and pagination verified after compatibility fix; initial balance required reload |
|
|
53
53
|
| Cursor, Gemini CLI, goose | Executables unavailable in this environment | Not tested |
|
|
54
54
|
| ChatGPT hosted surfaces | No host canary performed | Not tested |
|
|
55
55
|
|
|
@@ -67,4 +67,29 @@ A temporary public tunnel allowed discovery but failed tool execution before rea
|
|
|
67
67
|
|
|
68
68
|
For Windows/WSL testing, a headed Linux browser may not appear on the Windows desktop. Use a native Windows test browser with a separate persistent profile and loopback CDP endpoint. Reuse one Playwright CDP connection. If existing cross-origin frames are missing from automation, reload with that connection established; Claude uses a wrapper frame and a nested `about:blank` app frame. Never commit browser profiles or credentials.
|
|
69
69
|
|
|
70
|
-
Conversational rendering is verified for this exact surface.
|
|
70
|
+
Conversational rendering is verified for this exact surface. Cursor interactive rendering and authenticated staging reconnect/account-isolation remain separate open gates.
|
|
71
|
+
|
|
72
|
+
## VS Code Copilot and session compatibility (0.17.3)
|
|
73
|
+
|
|
74
|
+
Native Windows VS Code 1.135.0 advertised Apps MIME support but omitted MCP-Protocol-Version on subsequent requests. The shared handler now applies the specified 2025-03-26 fallback for an absent header; explicitly unsupported values still return 400. The fixture now uses default supported protocols instead of forcing a downgrade. See the [MCP transport specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header).
|
|
75
|
+
|
|
76
|
+
Three report views rendered in the signed-in isolated Copilot workspace. Balance and usage refresh, daily expansion and receipt pagination passed. There were six report calls: three initial calls and three explicit button actions. Reloading restored the original report results with no extra report calls. All app documents measured 242px without horizontal document overflow. The first balance webview was blank until a window reload; its cause is not isolated, so this is a qualified rendering result, not proof of reliable first-load behavior. Reviewed aggregate evidence: `canary/results/2026-09-11-vscode.json`.
|
|
77
|
+
|
|
78
|
+
Package regressions verify authorization before session deletion, per-request account selection, expired-session HTTP 404, and fresh explicit client initialization. HTTP failures now produce McpClientError with status (and a JSON-RPC code when available), including empty/non-JSON responses. After a session 404, call client.initialize() to negotiate a fresh session before further operations. The failed tool is never automatically replayed; callers must decide whether retrying is appropriate. Synthetic credentials prove handler behavior, not OAuth correctness or real-tenant isolation. Authenticated staging remains a separate release gate.
|
|
79
|
+
|
|
80
|
+
## Authenticated two-account staging canary
|
|
81
|
+
|
|
82
|
+
Import `runAuthenticatedCanary` from `@absolutejs/mcp/canary/authenticated` in a local Bun script. Supply an HTTPS endpoint, two distinct access tokens obtained through the application's normal OAuth flow, an explicitly read-only tool, and a fingerprint function selecting stable account-owned data. Credentials, fingerprints and results remain in memory; the returned report contains only check names and booleans. Keep the calling script and tokens outside the repository.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const report = await runAuthenticatedCanary({
|
|
86
|
+
url: stagingMcpUrl,
|
|
87
|
+
accounts: [firstAccessToken, secondAccessToken],
|
|
88
|
+
tool: { name: accountReadTool, arguments: {} },
|
|
89
|
+
fingerprint: (result) => JSON.stringify(result.content),
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Choose two accounts with known distinct data. The canary refuses identical results as inconclusive and verifies repeated reads remain stable. It checks both directions of session-ID substitution: rejecting the foreign session or returning the currently authenticated account's result is acceptable. It also checks missing/invalid credentials, unauthorized DELETE, authorized deletion, terminated-session 404, and fresh initialization/read. It creates and cleans up only its own MCP sessions, never purchases or modifies account data. Read-only annotations are a prerequisite, not a substitute for the operator selecting a known non-billable read tool. The current harness requires JSON RPC responses; SSE-only servers are not certified by it.
|
|
94
|
+
|
|
95
|
+
This verifies the selected read path, not every tenant resource, token revocation, automatic host reconnect, or billing permissions. Session termination is explicit DELETE; timed TTL expiry needs a separate store test. Use dedicated test clients and remove their grants after verification. Do not publish tokens, transcripts, fingerprints, or raw account data.
|
package/package.json
CHANGED
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"types": "./dist/src/apps.d.ts",
|
|
47
47
|
"import": "./dist/apps.js",
|
|
48
48
|
"default": "./dist/apps.js"
|
|
49
|
-
}
|
|
49
|
+
},
|
|
50
|
+
"./canary/authenticated": "./canary/authenticated.ts"
|
|
50
51
|
},
|
|
51
52
|
"publishConfig": {
|
|
52
53
|
"access": "public"
|
|
@@ -83,8 +84,8 @@
|
|
|
83
84
|
"prepublishOnly": "bun run check:package",
|
|
84
85
|
"build:apps": "bun scripts/build-apps.ts",
|
|
85
86
|
"canary": "bun canary/server.ts",
|
|
86
|
-
"check:canary": "tsc --noEmit --strict --skipLibCheck --moduleResolution bundler --module esnext --target esnext --types bun canary/server.ts"
|
|
87
|
+
"check:canary": "tsc --noEmit --strict --skipLibCheck --moduleResolution bundler --module esnext --target esnext --types bun canary/server.ts canary/authenticated.ts"
|
|
87
88
|
},
|
|
88
89
|
"types": "./dist/src/index.d.ts",
|
|
89
|
-
"version": "0.17.
|
|
90
|
+
"version": "0.17.4"
|
|
90
91
|
}
|