@plitzi/sdk-server 0.32.14 → 0.32.15
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
CHANGED
|
@@ -43,7 +43,7 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
|
|
|
43
43
|
const getSpace = () => spacePromise ??= loadSpace();
|
|
44
44
|
const server = new McpServer({
|
|
45
45
|
name: "plitzi-mcp",
|
|
46
|
-
version: "0.32.
|
|
46
|
+
version: "0.32.15"
|
|
47
47
|
}, { instructions: serverInstructions });
|
|
48
48
|
registerResources(server, getSpace, MCP_ENV, log);
|
|
49
49
|
registerApps(server);
|
|
@@ -16,6 +16,15 @@ var writeJson = async (store, kind, id, value, ttlSeconds) => {
|
|
|
16
16
|
};
|
|
17
17
|
var putClient = (store, client) => writeJson(store, "client", client.clientId, client, CLIENT_TTL_SECONDS);
|
|
18
18
|
var getClient = (store, clientId) => readJson(store, "client", clientId);
|
|
19
|
+
/** The id already handed out for an identical registration. Keyed by what the client asked to be registered AS,
|
|
20
|
+
* so the same request always resolves to the same client — see handleRegister for why that matters. */
|
|
21
|
+
var getClientByFingerprint = async (store, fingerprint) => {
|
|
22
|
+
const clientId = await store.get(keyOf("clientfp", fingerprint));
|
|
23
|
+
return clientId ? getClient(store, clientId) : void 0;
|
|
24
|
+
};
|
|
25
|
+
var putClientFingerprint = async (store, fingerprint, clientId) => {
|
|
26
|
+
await store.put(keyOf("clientfp", fingerprint), clientId, CLIENT_TTL_SECONDS);
|
|
27
|
+
};
|
|
19
28
|
var putPending = (store, id, pending) => writeJson(store, "pending", id, pending, PENDING_TTL_SECONDS);
|
|
20
29
|
var getPending = (store, id) => readJson(store, "pending", id);
|
|
21
30
|
var dropPending = async (store, id) => {
|
|
@@ -34,4 +43,4 @@ var dropRefresh = async (store, token) => {
|
|
|
34
43
|
var putAccess = (store, token, record, ttlSeconds) => writeJson(store, "access", token, record, ttlSeconds);
|
|
35
44
|
var getAccess = (store, token) => readJson(store, "access", token);
|
|
36
45
|
//#endregion
|
|
37
|
-
export { dropCode, dropPending, dropRefresh, getAccess, getClient, getCode, getPending, getRefresh, putAccess, putClient, putCode, putPending, putRefresh };
|
|
46
|
+
export { dropCode, dropPending, dropRefresh, getAccess, getClient, getClientByFingerprint, getCode, getPending, getRefresh, putAccess, putClient, putClientFingerprint, putCode, putPending, putRefresh };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomId } from "./pkce.js";
|
|
2
|
-
import { putClient } from "./records.js";
|
|
2
|
+
import { getClientByFingerprint, putClient, putClientFingerprint } from "./records.js";
|
|
3
3
|
import { sendErrorJson, sendJson } from "./respond.js";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
4
5
|
//#region src/modules/oauth/register.ts
|
|
5
6
|
var isUsableRedirectUri = (value) => {
|
|
6
7
|
let url;
|
|
@@ -13,6 +14,8 @@ var isUsableRedirectUri = (value) => {
|
|
|
13
14
|
return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
|
|
14
15
|
};
|
|
15
16
|
var stringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
17
|
+
var nowSeconds = () => Math.floor(Date.now() / 1e3);
|
|
18
|
+
var fingerprintOf = (clientName, redirectUris) => createHash("sha256").update(JSON.stringify([clientName, [...redirectUris].sort()])).digest("base64url");
|
|
16
19
|
/** RFC 7591 dynamic client registration. A remote host has no way to be configured into this server ahead of
|
|
17
20
|
* time, so it registers itself on first connect; the record it gets back is only ever used to pin the redirect
|
|
18
21
|
* target, since a public client authenticates with PKCE rather than with credentials. */
|
|
@@ -28,15 +31,19 @@ var handleRegister = async (config, res, body) => {
|
|
|
28
31
|
return;
|
|
29
32
|
}
|
|
30
33
|
const clientName = typeof metadata["client_name"] === "string" ? metadata["client_name"] : "MCP client";
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const { store } = config.adapters;
|
|
35
|
+
const fingerprint = fingerprintOf(clientName, redirectUris);
|
|
36
|
+
const client = await getClientByFingerprint(store, fingerprint) ?? {
|
|
37
|
+
clientId: randomId(),
|
|
34
38
|
clientName,
|
|
35
|
-
redirectUris
|
|
36
|
-
|
|
39
|
+
redirectUris,
|
|
40
|
+
issuedAt: nowSeconds()
|
|
41
|
+
};
|
|
42
|
+
await putClient(store, client);
|
|
43
|
+
await putClientFingerprint(store, fingerprint, client.clientId);
|
|
37
44
|
sendJson(res, 201, {
|
|
38
|
-
client_id: clientId,
|
|
39
|
-
client_id_issued_at:
|
|
45
|
+
client_id: client.clientId,
|
|
46
|
+
client_id_issued_at: client.issuedAt,
|
|
40
47
|
client_name: clientName,
|
|
41
48
|
redirect_uris: redirectUris,
|
|
42
49
|
grant_types: config.refreshTtlSeconds === 0 ? ["authorization_code"] : ["authorization_code", "refresh_token"],
|
|
@@ -3,13 +3,13 @@ import { renderErrorPage } from "./consentPage.js";
|
|
|
3
3
|
var sendJson = (res, status, body) => {
|
|
4
4
|
res.setStatus(status);
|
|
5
5
|
res.setHeader("Content-Type", "application/json");
|
|
6
|
-
res.setHeader("Cache-Control", "no-store");
|
|
6
|
+
res.setHeader("Cache-Control", "no-store, no-transform");
|
|
7
7
|
res.send(JSON.stringify(body));
|
|
8
8
|
};
|
|
9
9
|
var sendHtml = (res, status, html) => {
|
|
10
10
|
res.setStatus(status);
|
|
11
11
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
12
|
-
res.setHeader("Cache-Control", "no-store");
|
|
12
|
+
res.setHeader("Cache-Control", "no-store, no-transform");
|
|
13
13
|
res.send(html);
|
|
14
14
|
};
|
|
15
15
|
var sendErrorJson = (res, status, error, description) => sendJson(res, status, {
|
|
@@ -28,7 +28,7 @@ var redirectWithError = (res, redirectUri, error, description, state) => {
|
|
|
28
28
|
if (state !== void 0) url.searchParams.set("state", state);
|
|
29
29
|
res.setStatus(302);
|
|
30
30
|
res.setHeader("Location", url.toString());
|
|
31
|
-
res.setHeader("Cache-Control", "no-store");
|
|
31
|
+
res.setHeader("Cache-Control", "no-store, no-transform");
|
|
32
32
|
res.end();
|
|
33
33
|
};
|
|
34
34
|
var redirectWithCode = (res, redirectUri, code, state) => {
|
|
@@ -37,7 +37,7 @@ var redirectWithCode = (res, redirectUri, code, state) => {
|
|
|
37
37
|
if (state !== void 0) url.searchParams.set("state", state);
|
|
38
38
|
res.setStatus(302);
|
|
39
39
|
res.setHeader("Location", url.toString());
|
|
40
|
-
res.setHeader("Cache-Control", "no-store");
|
|
40
|
+
res.setHeader("Cache-Control", "no-store, no-transform");
|
|
41
41
|
res.end();
|
|
42
42
|
};
|
|
43
43
|
//#endregion
|
|
@@ -5,6 +5,8 @@ export type ClientRecord = {
|
|
|
5
5
|
clientId: string;
|
|
6
6
|
clientName: string;
|
|
7
7
|
redirectUris: string[];
|
|
8
|
+
/** When this registration was first handed out, so re-registering the same client keeps reporting its real age. */
|
|
9
|
+
issuedAt: number;
|
|
8
10
|
};
|
|
9
11
|
/** The authorization request, parked while the user logs in. `challenge` binds the eventual code to the client
|
|
10
12
|
* that started the flow. */
|
|
@@ -52,6 +54,10 @@ export type AccessRecord = {
|
|
|
52
54
|
};
|
|
53
55
|
export declare const putClient: (store: OAuthStore, client: ClientRecord) => Promise<void>;
|
|
54
56
|
export declare const getClient: (store: OAuthStore, clientId: string) => Promise<ClientRecord | undefined>;
|
|
57
|
+
/** The id already handed out for an identical registration. Keyed by what the client asked to be registered AS,
|
|
58
|
+
* so the same request always resolves to the same client — see handleRegister for why that matters. */
|
|
59
|
+
export declare const getClientByFingerprint: (store: OAuthStore, fingerprint: string) => Promise<ClientRecord | undefined>;
|
|
60
|
+
export declare const putClientFingerprint: (store: OAuthStore, fingerprint: string, clientId: string) => Promise<void>;
|
|
55
61
|
export declare const putPending: (store: OAuthStore, id: string, pending: PendingRecord) => Promise<void>;
|
|
56
62
|
export declare const getPending: (store: OAuthStore, id: string) => Promise<PendingRecord | undefined>;
|
|
57
63
|
export declare const dropPending: (store: OAuthStore, id: string) => Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plitzi/sdk-server",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.15",
|
|
4
4
|
"license": "AGPL-3.0",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
32
|
-
"@plitzi/plitzi-sdk": "0.32.
|
|
33
|
-
"@plitzi/sdk-schema": "0.32.
|
|
34
|
-
"@plitzi/sdk-shared": "0.32.
|
|
32
|
+
"@plitzi/plitzi-sdk": "0.32.15",
|
|
33
|
+
"@plitzi/sdk-schema": "0.32.15",
|
|
34
|
+
"@plitzi/sdk-shared": "0.32.15",
|
|
35
35
|
"ejs": "^6.0.1",
|
|
36
36
|
"esbuild": "^0.28.1",
|
|
37
37
|
"zod": "^4.4.3"
|