@plitzi/sdk-server 0.32.10 → 0.32.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/core/http/dispatcher.js +48 -10
- package/dist/core/requestParser.js +18 -1
- package/dist/core/services/mcp.js +4 -4
- package/dist/core/services/oauth.js +71 -0
- package/dist/core/services/registry.js +2 -0
- package/dist/core/services/rsc.js +1 -0
- package/dist/helpers/serverLog.js +34 -0
- package/dist/index.js +2 -1
- package/dist/mcp.js +2 -1
- package/dist/modules/mcp/handler.js +10 -0
- package/dist/modules/mcp/helpers/log.js +20 -10
- package/dist/modules/mcp/resources/register.js +10 -4
- package/dist/modules/mcp/resources/renderGuide.js +17 -7
- package/dist/modules/mcp/server.js +1 -1
- package/dist/modules/oauth/authorize.js +160 -0
- package/dist/modules/oauth/consentPage.js +111 -0
- package/dist/modules/oauth/metadata.js +43 -0
- package/dist/modules/oauth/params.js +5 -0
- package/dist/modules/oauth/pkce.js +15 -0
- package/dist/modules/oauth/records.js +35 -0
- package/dist/modules/oauth/register.js +48 -0
- package/dist/modules/oauth/respond.js +44 -0
- package/dist/modules/oauth/token.js +92 -0
- package/dist/plugins/manager.js +2 -0
- package/dist/plugins/validate.js +23 -0
- package/dist/src/core/http/dispatcher.test.d.ts +1 -0
- package/dist/src/core/http/types.d.ts +3 -0
- package/dist/src/core/requestParser.d.ts +5 -0
- package/dist/src/core/services/oauth.d.ts +8 -0
- package/dist/src/helpers/buildResponseHelpers.d.ts +2 -0
- package/dist/src/helpers/serverLog.d.ts +10 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/mcp.d.ts +1 -0
- package/dist/src/modules/mcp/handler.d.ts +4 -4
- package/dist/src/modules/mcp/helpers/log.d.ts +3 -3
- package/dist/src/modules/mcp/resources/renderGuide.d.ts +2 -1
- package/dist/src/modules/mcp/server.d.ts +2 -2
- package/dist/src/modules/oauth/authorize.d.ts +7 -0
- package/dist/src/modules/oauth/consentPage.d.ts +7 -0
- package/dist/src/modules/oauth/metadata.d.ts +17 -0
- package/dist/src/modules/oauth/oauth.test.d.ts +1 -0
- package/dist/src/modules/oauth/params.d.ts +5 -0
- package/dist/src/modules/oauth/pkce.d.ts +6 -0
- package/dist/src/modules/oauth/records.d.ts +49 -0
- package/dist/src/modules/oauth/register.d.ts +5 -0
- package/dist/src/modules/oauth/respond.d.ts +13 -0
- package/dist/src/modules/oauth/token.d.ts +5 -0
- package/dist/src/plugins/validate.d.ts +9 -0
- package/dist/src/plugins/validate.test.d.ts +1 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @plitzi/sdk-server
|
|
2
2
|
|
|
3
|
+
## 0.32.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- v0.32.12
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @plitzi/plitzi-sdk@0.32.12
|
|
10
|
+
- @plitzi/sdk-schema@0.32.12
|
|
11
|
+
- @plitzi/sdk-shared@0.32.12
|
|
12
|
+
|
|
13
|
+
## 0.32.11
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- v0.32.11
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
- @plitzi/plitzi-sdk@0.32.11
|
|
20
|
+
- @plitzi/sdk-schema@0.32.11
|
|
21
|
+
- @plitzi/sdk-shared@0.32.11
|
|
22
|
+
|
|
3
23
|
## 0.32.10
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
|
@@ -1,22 +1,60 @@
|
|
|
1
1
|
import { applySecurityHeaders } from "./securityHeaders.js";
|
|
2
2
|
import { buildResponseHelpers } from "../../helpers/buildResponseHelpers.js";
|
|
3
|
-
import { parseRequest } from "../requestParser.js";
|
|
3
|
+
import { clientIp, parseRequest } from "../requestParser.js";
|
|
4
4
|
//#region src/core/http/dispatcher.ts
|
|
5
|
-
var
|
|
5
|
+
var safePath = (url) => {
|
|
6
|
+
const [path = "/", query] = url.split("?");
|
|
7
|
+
if (!query) return path;
|
|
8
|
+
const keys = [...new Set(new URLSearchParams(query).keys())];
|
|
9
|
+
return keys.length > 0 ? `${path}?${keys.join("&")}` : path;
|
|
10
|
+
};
|
|
11
|
+
var errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
12
|
+
var statusOf = (rawRes, res) => rawRes.headersSent ? rawRes.statusCode : res.status;
|
|
13
|
+
var runPipeline = async (raw, rawRes, buildContext, stages, server) => {
|
|
14
|
+
const startedAt = Date.now();
|
|
6
15
|
const req = parseRequest(raw);
|
|
7
16
|
const res = buildResponseHelpers(rawRes, req.headers["accept-encoding"]);
|
|
8
|
-
if (req.path === "\0") {
|
|
9
|
-
res.setStatus(400);
|
|
10
|
-
res.end();
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
17
|
const ctx = buildContext(raw, rawRes, req, res);
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
const logger = ctx.config.logger;
|
|
19
|
+
const ip = logger ? clientIp(raw, req) : "";
|
|
20
|
+
const logRequest = (error) => {
|
|
21
|
+
if (!logger) return;
|
|
22
|
+
const status = statusOf(rawRes, res);
|
|
23
|
+
logger({
|
|
24
|
+
kind: "request",
|
|
25
|
+
server,
|
|
26
|
+
method: req.method,
|
|
27
|
+
path: safePath(req.url),
|
|
28
|
+
...ip ? { clientIp: ip } : {},
|
|
29
|
+
...ctx.operation === void 0 ? {} : { operation: ctx.operation },
|
|
30
|
+
status,
|
|
31
|
+
durationMs: Date.now() - startedAt,
|
|
32
|
+
ok: !error && status < 400,
|
|
33
|
+
...error ? { error: errorText(error) } : {},
|
|
34
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
if (req.path === "\0") {
|
|
39
|
+
res.setStatus(400);
|
|
40
|
+
res.end();
|
|
41
|
+
logRequest();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
applySecurityHeaders(res, ctx.config, ctx.port);
|
|
45
|
+
for (const stage of stages) if (await stage(ctx)) {
|
|
46
|
+
logRequest();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
logRequest();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logRequest(error);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
16
54
|
};
|
|
17
55
|
var makeHandler = (label, buildContext, stages) => {
|
|
18
56
|
return (raw, rawRes) => {
|
|
19
|
-
runPipeline(raw, rawRes, buildContext, stages).catch((err) => {
|
|
57
|
+
runPipeline(raw, rawRes, buildContext, stages, label).catch((err) => {
|
|
20
58
|
console.error(`[${label}] Unhandled error:`, err);
|
|
21
59
|
try {
|
|
22
60
|
if (!rawRes.headersSent) rawRes.writeHead(500, { "Content-Type": "text/plain" });
|
|
@@ -30,6 +30,23 @@ var parseRequest = (raw) => {
|
|
|
30
30
|
ctx: {}
|
|
31
31
|
};
|
|
32
32
|
};
|
|
33
|
+
var AUTHORITY_RE = /^[a-zA-Z0-9.-]{1,253}(?::\d{1,5})?$/u;
|
|
34
|
+
/** The public origin the request was addressed to (proxy-aware via x-forwarded-proto, port included), or an empty
|
|
35
|
+
* string when the request carries no usable authority. */
|
|
36
|
+
var requestOrigin = (req) => {
|
|
37
|
+
const authority = req.headers[":authority"] ?? req.headers["host"] ?? "";
|
|
38
|
+
return AUTHORITY_RE.test(authority) ? `${req.protocol}://${authority}` : "";
|
|
39
|
+
};
|
|
40
|
+
var headerValue = (value) => (Array.isArray(value) ? value[0] : value)?.trim() ?? "";
|
|
41
|
+
var unmapIpv4 = (address) => address.startsWith("::ffff:") && address.includes(".") ? address.slice(7) : address;
|
|
42
|
+
/** The address the request came from, seen through the proxies a deployment sits behind: Cloudflare states the
|
|
43
|
+
* peer it accepted in `cf-connecting-ip`, the ingress in `x-real-ip`, and `x-forwarded-for` lists the chain with
|
|
44
|
+
* the original client first. All three are plain headers a direct client can forge, so this is log/diagnostic
|
|
45
|
+
* material — never an authorisation input. Falls back to the socket peer, which no client controls. */
|
|
46
|
+
var clientIp = (raw, req) => {
|
|
47
|
+
const forwardedFor = headerValue(req.headers["x-forwarded-for"]).split(",")[0] ?? "";
|
|
48
|
+
return unmapIpv4(headerValue(req.headers["cf-connecting-ip"]) || headerValue(req.headers["x-real-ip"]) || forwardedFor.trim() || raw.socket.remoteAddress || "");
|
|
49
|
+
};
|
|
33
50
|
var MAX_BODY_BYTES = 1024 * 1024;
|
|
34
51
|
var readRawBody = (raw) => new Promise((resolve, reject) => {
|
|
35
52
|
const chunks = [];
|
|
@@ -46,4 +63,4 @@ var readRawBody = (raw) => new Promise((resolve, reject) => {
|
|
|
46
63
|
raw.on("error", reject);
|
|
47
64
|
});
|
|
48
65
|
//#endregion
|
|
49
|
-
export { parseRequest, readRawBody };
|
|
66
|
+
export { clientIp, parseRequest, readRawBody, requestOrigin };
|
|
@@ -3,12 +3,12 @@ import { createHttpPreviewClient } from "../../modules/mcp/previewClient.js";
|
|
|
3
3
|
import { createHttpScreenshotClient } from "../../modules/mcp/screenshotClient.js";
|
|
4
4
|
//#region src/core/services/mcp.ts
|
|
5
5
|
var mcpPathOf = (config) => config.mcpAi?.path ?? config.mcp?.path ?? "/mcp";
|
|
6
|
-
var serveMcp = (ctx) => {
|
|
7
|
-
const { previewClient, screenshot, adapters,
|
|
8
|
-
|
|
6
|
+
var serveMcp = async (ctx) => {
|
|
7
|
+
const { previewClient, screenshot, adapters, logger } = ctx.config;
|
|
8
|
+
ctx.operation = await handleMcp(ctx.raw, ctx.rawRes, ctx.req, adapters, {
|
|
9
9
|
preview: previewClient ? createHttpPreviewClient(previewClient) : void 0,
|
|
10
10
|
screenshot: screenshot ? createHttpScreenshotClient(screenshot) : void 0,
|
|
11
|
-
logger
|
|
11
|
+
logger
|
|
12
12
|
});
|
|
13
13
|
};
|
|
14
14
|
var mcpStage = async (ctx) => {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readRawBody } from "../requestParser.js";
|
|
2
|
+
import { authorizationServerMetadata, protectedResourceMetadata } from "../../modules/oauth/metadata.js";
|
|
3
|
+
import { sendErrorJson, sendJson } from "../../modules/oauth/respond.js";
|
|
4
|
+
import { handleAuthorizeStart, handleAuthorizeSubmit } from "../../modules/oauth/authorize.js";
|
|
5
|
+
import { handleRegister } from "../../modules/oauth/register.js";
|
|
6
|
+
import { handleToken } from "../../modules/oauth/token.js";
|
|
7
|
+
//#region src/core/services/oauth.ts
|
|
8
|
+
var matchesWellKnown = (path, wellKnown) => path === wellKnown || path.startsWith(`${wellKnown}/`);
|
|
9
|
+
var isOAuthPath = (path) => matchesWellKnown(path, "/.well-known/oauth-protected-resource") || matchesWellKnown(path, "/.well-known/oauth-authorization-server") || path === "/register" || path === "/authorize" || path === "/token";
|
|
10
|
+
var formParams = (req, body) => {
|
|
11
|
+
const params = { ...req.query };
|
|
12
|
+
for (const [key, value] of new URLSearchParams(body).entries()) params[key] = value;
|
|
13
|
+
return params;
|
|
14
|
+
};
|
|
15
|
+
var parseJson = (body) => {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(body);
|
|
18
|
+
} catch {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/** OAuth 2.1 authorization for the MCP server, mounted ONLY when a deployment configures `oauth`. Without it the
|
|
23
|
+
* stage falls straight through and the server keeps its anonymous contract: discovery 404s, the public surface
|
|
24
|
+
* (handshake, listings, the guide, plitzi_render) answers without a token, and nothing below changes.
|
|
25
|
+
*
|
|
26
|
+
* It sits before the MCP stage because that one answers every path on a dedicated MCP server — these endpoints
|
|
27
|
+
* would otherwise be swallowed by the JSON-RPC transport, which is exactly the 406 a host hits on /register. */
|
|
28
|
+
var oauthStage = async (ctx) => {
|
|
29
|
+
const { oauth } = ctx.config;
|
|
30
|
+
const { path, method } = ctx.req;
|
|
31
|
+
if (!oauth || !isOAuthPath(path)) return false;
|
|
32
|
+
const { res } = ctx;
|
|
33
|
+
if (path !== "/authorize") {
|
|
34
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
35
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
36
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
37
|
+
}
|
|
38
|
+
if (method === "OPTIONS") {
|
|
39
|
+
res.setStatus(204);
|
|
40
|
+
res.end();
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (method === "GET" && matchesWellKnown(path, "/.well-known/oauth-protected-resource")) {
|
|
44
|
+
sendJson(res, 200, protectedResourceMetadata(oauth, ctx.req));
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (method === "GET" && matchesWellKnown(path, "/.well-known/oauth-authorization-server")) {
|
|
48
|
+
sendJson(res, 200, authorizationServerMetadata(oauth, ctx.req));
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (path === "/authorize" && method === "GET") {
|
|
52
|
+
await handleAuthorizeStart(oauth, res, ctx.req.query);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (path === "/authorize" && method === "POST") {
|
|
56
|
+
await handleAuthorizeSubmit(oauth, res, formParams(ctx.req, await readRawBody(ctx.raw)));
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (path === "/token" && method === "POST") {
|
|
60
|
+
await handleToken(oauth, res, formParams(ctx.req, await readRawBody(ctx.raw)));
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (path === "/register" && method === "POST") {
|
|
64
|
+
await handleRegister(oauth, res, parseJson(await readRawBody(ctx.raw)));
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
sendErrorJson(res, 405, "invalid_request", `${method} is not allowed on ${path}.`);
|
|
68
|
+
return true;
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
export { oauthStage };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mcpOnlyStage, mcpStage } from "./mcp.js";
|
|
2
|
+
import { oauthStage } from "./oauth.js";
|
|
2
3
|
import { previewStage } from "./preview.js";
|
|
3
4
|
import { rscStage } from "./rsc.js";
|
|
4
5
|
import { notFoundStage, ssrStage } from "./ssr.js";
|
|
@@ -28,6 +29,7 @@ var buildSSRPipeline = (services) => {
|
|
|
28
29
|
var buildMCPPipeline = () => [
|
|
29
30
|
healthStage,
|
|
30
31
|
configStaticStage,
|
|
32
|
+
oauthStage,
|
|
31
33
|
mcpOnlyStage
|
|
32
34
|
];
|
|
33
35
|
//#endregion
|
|
@@ -4,6 +4,7 @@ var rscStage = async (ctx) => {
|
|
|
4
4
|
const { config, req } = ctx;
|
|
5
5
|
const rscPath = config.rsc?.path ?? "/_rsc";
|
|
6
6
|
if (!(config.rsc?.enabled ?? true) || req.method !== "GET" || req.path !== rscPath) return false;
|
|
7
|
+
ctx.operation = "rsc";
|
|
7
8
|
await handleRsc(req, ctx.res, config, ctx.pluginManager, ctx.caches.rsc);
|
|
8
9
|
return true;
|
|
9
10
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/helpers/serverLog.ts
|
|
2
|
+
var outcomeOf = (event) => event.ok ? "ok" : `ERROR ${event.error ?? ""}`.trim();
|
|
3
|
+
var renderRequest = (event) => {
|
|
4
|
+
const client = event.clientIp ? `${event.clientIp} ` : "";
|
|
5
|
+
const operation = event.operation ? ` ${event.operation}` : "";
|
|
6
|
+
const timing = `${Math.round(event.durationMs)}ms`;
|
|
7
|
+
return `[${event.server}] ${client}${event.method} ${event.path}${operation} ${event.status} ${timing} ${outcomeOf(event)}`;
|
|
8
|
+
};
|
|
9
|
+
var renderTool = (event) => {
|
|
10
|
+
const args = event.argsSummary ? ` ${event.argsSummary}` : "";
|
|
11
|
+
return `[mcp] tools/call ${event.name}${args} ${Math.round(event.durationMs)}ms ${outcomeOf(event)}`;
|
|
12
|
+
};
|
|
13
|
+
var renderResource = (event) => `[mcp] resources/read ${event.name} ${Math.round(event.durationMs)}ms ${outcomeOf(event)}`;
|
|
14
|
+
/** One line for any {@link ServerLogEvent}: an HTTP request reads as an access-log line
|
|
15
|
+
* (`[SSR] 203.0.113.7 GET /pricing 200 12ms ok`), the MCP events as what happened inside one
|
|
16
|
+
* (`[mcp] tools/call plitzi_apply {operations:[3]} 41ms ok`). Rendering is a pure format — the dispatcher
|
|
17
|
+
* already stripped query values, collected no headers, cookies or tokens and summarised tool args by shape;
|
|
18
|
+
* the client IP it does carry is personal data, so a sink that persists these lines must say so. */
|
|
19
|
+
var renderLogEvent = (event) => {
|
|
20
|
+
switch (event.kind) {
|
|
21
|
+
case "request": return renderRequest(event);
|
|
22
|
+
case "tool": return renderTool(event);
|
|
23
|
+
case "resource": return renderResource(event);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/** A drop-in `SSRServerConfig.logger` for consumers that just want the log on the console. Consumers with their
|
|
27
|
+
* own logging stack should pass their own sink instead and read the structured event. */
|
|
28
|
+
var consoleLogger = (event) => {
|
|
29
|
+
const line = renderLogEvent(event);
|
|
30
|
+
if (event.ok) console.log(line);
|
|
31
|
+
else console.error(line);
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { consoleLogger, renderLogEvent };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildAgentGuide } from "./modules/mcp/helpers/agentPrompt.js";
|
|
2
|
+
import { consoleLogger, renderLogEvent } from "./helpers/serverLog.js";
|
|
2
3
|
import { operation } from "./modules/mcp/tools/operations/index.js";
|
|
3
4
|
import { apply, applyShape } from "./modules/mcp/tools/apply/index.js";
|
|
4
5
|
import { read, readShape } from "./modules/mcp/tools/read.js";
|
|
@@ -19,4 +20,4 @@ import { createSSRServer } from "./core/server/ssrServer.js";
|
|
|
19
20
|
import { createServer } from "./core/createServer.js";
|
|
20
21
|
import { createJsonAdapters } from "./adapters/jsonAdapters.js";
|
|
21
22
|
import AIEngine from "./modules/ai/AIEngine.js";
|
|
22
|
-
export { AIEngine, PREVIEW_TOKEN_PARAM, apply, applyShape, bindTools, buildAgentGuide, buildHealthPayload, createHttpPreviewClient, createHttpScreenshotClient, createJsonAdapters, createMCPServer, createMcpServer, createMemoryDraftStore, createPreview, createSSRServer, createServer, getAllowedModes, handleMcp, isCallToolResult, isToolActive, operation, read, readMcpBody, readShape, registerHealthCheck, resolveServices, resolveToolHandler, search, searchShape, serveMcp, toolResponseErr, toolResponseFromResult, toolResponseOk, tools, validate, validateShape, zodToJsonSchema };
|
|
23
|
+
export { AIEngine, PREVIEW_TOKEN_PARAM, apply, applyShape, bindTools, buildAgentGuide, buildHealthPayload, consoleLogger, createHttpPreviewClient, createHttpScreenshotClient, createJsonAdapters, createMCPServer, createMcpServer, createMemoryDraftStore, createPreview, createSSRServer, createServer, getAllowedModes, handleMcp, isCallToolResult, isToolActive, operation, read, readMcpBody, readShape, registerHealthCheck, renderLogEvent, resolveServices, resolveToolHandler, search, searchShape, serveMcp, toolResponseErr, toolResponseFromResult, toolResponseOk, tools, validate, validateShape, zodToJsonSchema };
|
package/dist/mcp.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { consoleLogger, renderLogEvent } from "./helpers/serverLog.js";
|
|
1
2
|
import { createMcpServer } from "./modules/mcp/server.js";
|
|
2
3
|
import { handleMcp, readMcpBody, serveMcp } from "./modules/mcp/handler.js";
|
|
3
4
|
import { createHttpPreviewClient } from "./modules/mcp/previewClient.js";
|
|
4
5
|
import { createHttpScreenshotClient } from "./modules/mcp/screenshotClient.js";
|
|
5
6
|
import { createMCPServer } from "./core/server/mcpServer.js";
|
|
6
|
-
export { createHttpPreviewClient, createHttpScreenshotClient, createMCPServer, createMcpServer, handleMcp, readMcpBody, serveMcp };
|
|
7
|
+
export { consoleLogger, createHttpPreviewClient, createHttpScreenshotClient, createMCPServer, createMcpServer, handleMcp, readMcpBody, renderLogEvent, serveMcp };
|
|
@@ -15,6 +15,10 @@ var readMcpBody = (req) => new Promise((resolve, reject) => {
|
|
|
15
15
|
});
|
|
16
16
|
req.on("error", reject);
|
|
17
17
|
});
|
|
18
|
+
var operationOf = (body) => {
|
|
19
|
+
if (Array.isArray(body)) return `batch(${body.length})`;
|
|
20
|
+
if (typeof body === "object" && body !== null && "method" in body && typeof body.method === "string") return body.method;
|
|
21
|
+
};
|
|
18
22
|
var serveMcp = async (raw, res, server) => {
|
|
19
23
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
20
24
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
@@ -30,6 +34,11 @@ var serveMcp = async (raw, res, server) => {
|
|
|
30
34
|
res.end();
|
|
31
35
|
return;
|
|
32
36
|
}
|
|
37
|
+
if (raw.url === "/favicon.ico") {
|
|
38
|
+
res.writeHead(204);
|
|
39
|
+
res.end();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
33
42
|
if (raw.method !== "POST") {
|
|
34
43
|
res.writeHead(405, {
|
|
35
44
|
"Content-Type": "application/json",
|
|
@@ -60,6 +69,7 @@ var serveMcp = async (raw, res, server) => {
|
|
|
60
69
|
return;
|
|
61
70
|
}
|
|
62
71
|
await transport.handleRequest(raw, res, body);
|
|
72
|
+
return operationOf(body);
|
|
63
73
|
} finally {
|
|
64
74
|
await transport.close();
|
|
65
75
|
}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
import { renderLogEvent } from "../../../helpers/serverLog.js";
|
|
1
2
|
//#region src/modules/mcp/helpers/log.ts
|
|
2
3
|
var MCP_DEBUG = process.env.MCP_DEBUG === "1";
|
|
3
|
-
var
|
|
4
|
-
|
|
4
|
+
var MCP_LOG_ARGS = process.env.MCP_LOG_ARGS === "1";
|
|
5
|
+
var shapeOf = (value, depth = 0) => {
|
|
6
|
+
if (value === null) return "null";
|
|
7
|
+
if (Array.isArray(value)) return `[${value.length}]`;
|
|
8
|
+
if (typeof value === "object") {
|
|
9
|
+
if (depth >= 2) return "{…}";
|
|
10
|
+
const entries = Object.entries(value);
|
|
11
|
+
const shown = entries.slice(0, 8).map(([key, item]) => `${key}:${shapeOf(item, depth + 1)}`);
|
|
12
|
+
return `{${[...shown, ...entries.length > shown.length ? ["…"] : []].join(",")}}`;
|
|
13
|
+
}
|
|
14
|
+
return typeof value;
|
|
15
|
+
};
|
|
16
|
+
var rawSummary = (value, max = 300) => {
|
|
5
17
|
let json;
|
|
6
18
|
try {
|
|
7
19
|
json = JSON.stringify(value);
|
|
@@ -10,25 +22,23 @@ var summarize = (value, max = 300) => {
|
|
|
10
22
|
}
|
|
11
23
|
return json.length > max ? `${json.slice(0, max)}…` : json;
|
|
12
24
|
};
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const detail = event.argsSummary ? ` ${event.argsSummary}` : "";
|
|
17
|
-
const status = event.ok ? "ok" : `ERROR ${event.error ?? ""}`;
|
|
18
|
-
console.log(`[mcp] ${kind} ${event.name}${detail} ${Math.round(event.durationMs)}ms ${status}`);
|
|
25
|
+
var summarize = (value) => {
|
|
26
|
+
if (value === void 0) return;
|
|
27
|
+
return MCP_LOG_ARGS ? rawSummary(value) : shapeOf(value);
|
|
19
28
|
};
|
|
29
|
+
var errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
20
30
|
var noop = () => void 0;
|
|
21
31
|
var inertLog = {
|
|
22
32
|
toolCall: noop,
|
|
23
33
|
resourceRead: noop
|
|
24
34
|
};
|
|
25
|
-
/** Build the
|
|
35
|
+
/** Build the protocol-log sink for one MCP server. Dispatches structured events to the consumer's `logger` when
|
|
26
36
|
* provided; otherwise renders to the console when MCP_DEBUG=1; otherwise a no-op. */
|
|
27
37
|
var createMcpLog = (logger) => {
|
|
28
38
|
if (!logger && !MCP_DEBUG) return inertLog;
|
|
29
39
|
const emit = (event) => {
|
|
30
40
|
if (logger) logger(event);
|
|
31
|
-
else
|
|
41
|
+
else console.log(renderLogEvent(event));
|
|
32
42
|
};
|
|
33
43
|
return {
|
|
34
44
|
toolCall: (name, args, ms, error) => emit({
|
|
@@ -21,19 +21,25 @@ var registerResources = (server, getSpace, env, log) => {
|
|
|
21
21
|
throw error;
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
|
+
const emitStatic = (uri, read) => {
|
|
25
|
+
const start = performance.now();
|
|
26
|
+
const result = read();
|
|
27
|
+
log.resourceRead(uri, performance.now() - start);
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
24
30
|
server.registerResource("Guide", "plitzi://guide", {
|
|
25
31
|
description: "How to read and write this space with mcp-ai",
|
|
26
32
|
mimeType: "text/markdown"
|
|
27
|
-
}, () => ({ contents: [{
|
|
33
|
+
}, () => emitStatic("plitzi://guide", () => ({ contents: [{
|
|
28
34
|
uri: "plitzi://guide",
|
|
29
35
|
mimeType: "text/markdown",
|
|
30
36
|
text: guideText
|
|
31
|
-
}] }));
|
|
37
|
+
}] })));
|
|
32
38
|
server.registerResource("CSS properties", "plitzi://css-properties", {
|
|
33
39
|
description: "Valid kebab-case CSS property keys",
|
|
34
40
|
mimeType: "application/json"
|
|
35
|
-
}, () => jsonContents("plitzi://css-properties", envelope(cssProperties)));
|
|
36
|
-
registerRenderResources(server);
|
|
41
|
+
}, () => emitStatic("plitzi://css-properties", () => jsonContents("plitzi://css-properties", envelope(cssProperties))));
|
|
42
|
+
registerRenderResources(server, log);
|
|
37
43
|
const fixed = [
|
|
38
44
|
[
|
|
39
45
|
"Primer",
|
|
@@ -168,19 +168,29 @@ connects that source to a descendant's field.
|
|
|
168
168
|
The tool returns \`rendered: false\` with \`errors: [{ path, message, hint }]\`. Read the hint, fix that one op, and
|
|
169
169
|
retry — you never lose the rest of the batch.
|
|
170
170
|
`;
|
|
171
|
-
var registerRenderResources = (server) => {
|
|
171
|
+
var registerRenderResources = (server, log) => {
|
|
172
172
|
server.registerResource("Render guide", RENDER_GUIDE_URI, {
|
|
173
173
|
description: "How to author a plitzi_render widget: operations, element types, styling, examples.",
|
|
174
174
|
mimeType: "text/markdown"
|
|
175
|
-
}, () =>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
175
|
+
}, () => {
|
|
176
|
+
const start = performance.now();
|
|
177
|
+
const contents = { contents: [{
|
|
178
|
+
uri: RENDER_GUIDE_URI,
|
|
179
|
+
mimeType: "text/markdown",
|
|
180
|
+
text: renderGuideText
|
|
181
|
+
}] };
|
|
182
|
+
log.resourceRead(RENDER_GUIDE_URI, performance.now() - start);
|
|
183
|
+
return contents;
|
|
184
|
+
});
|
|
180
185
|
server.registerResource("Render element types", RENDER_TYPES_URI, {
|
|
181
186
|
description: "Built-in element types a plitzi_render widget can use, with descriptions.",
|
|
182
187
|
mimeType: "application/json"
|
|
183
|
-
}, () =>
|
|
188
|
+
}, () => {
|
|
189
|
+
const start = performance.now();
|
|
190
|
+
const contents = jsonContents(RENDER_TYPES_URI, envelope(renderTypes()));
|
|
191
|
+
log.resourceRead(RENDER_TYPES_URI, performance.now() - start);
|
|
192
|
+
return contents;
|
|
193
|
+
});
|
|
184
194
|
};
|
|
185
195
|
//#endregion
|
|
186
196
|
export { RENDER_GUIDE_URI, RENDER_TYPES_URI, registerRenderResources };
|
|
@@ -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.12"
|
|
47
47
|
}, { instructions: serverInstructions });
|
|
48
48
|
registerResources(server, getSpace, MCP_ENV, log);
|
|
49
49
|
registerApps(server);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { renderConsentPage } from "./consentPage.js";
|
|
2
|
+
import { AUTHORIZE_PATH } from "./metadata.js";
|
|
3
|
+
import { field, optionalField } from "./params.js";
|
|
4
|
+
import { randomId } from "./pkce.js";
|
|
5
|
+
import { dropPending, getClient, getPending, putCode, putPending } from "./records.js";
|
|
6
|
+
import { redirectWithCode, redirectWithError, sendErrorPage, sendHtml } from "./respond.js";
|
|
7
|
+
//#region src/modules/oauth/authorize.ts
|
|
8
|
+
var DEFAULT_CODE_TTL_SECONDS = 60;
|
|
9
|
+
var hiddenFieldsFor = (request, pendingId) => {
|
|
10
|
+
const hidden = {
|
|
11
|
+
client_id: request.clientId,
|
|
12
|
+
redirect_uri: request.redirectUri,
|
|
13
|
+
code_challenge: request.challenge,
|
|
14
|
+
code_challenge_method: "S256"
|
|
15
|
+
};
|
|
16
|
+
if (request.state !== void 0) hidden["state"] = request.state;
|
|
17
|
+
if (request.scope !== void 0) hidden["scope"] = request.scope;
|
|
18
|
+
if (pendingId !== void 0) hidden["pending"] = pendingId;
|
|
19
|
+
return hidden;
|
|
20
|
+
};
|
|
21
|
+
var renderConsent = async (config, res, view) => {
|
|
22
|
+
sendHtml(res, 200, await (config.renderConsent ?? renderConsentPage)(view));
|
|
23
|
+
};
|
|
24
|
+
/** Validates the parts of an authorization request that decide WHERE a failure may be reported. Until the client
|
|
25
|
+
* and its redirect target check out, nothing may be sent back to the client. */
|
|
26
|
+
var resolveRequest = async (config, res, params) => {
|
|
27
|
+
const clientId = field(params, "client_id");
|
|
28
|
+
const redirectUri = field(params, "redirect_uri");
|
|
29
|
+
const client = clientId ? await getClient(config.adapters.store, clientId) : void 0;
|
|
30
|
+
if (!client) {
|
|
31
|
+
sendErrorPage(res, "Unknown client", "This application is not registered with the server, or its registration expired.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!redirectUri || !client.redirectUris.includes(redirectUri)) {
|
|
35
|
+
sendErrorPage(res, "Invalid redirect", "The application asked to be sent back to an address it did not register.");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const state = optionalField(params, "state");
|
|
39
|
+
const responseType = field(params, "response_type");
|
|
40
|
+
if (responseType && responseType !== "code") {
|
|
41
|
+
redirectWithError(res, redirectUri, "unsupported_response_type", "Only the authorization code flow is supported.", state);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const challenge = field(params, "code_challenge");
|
|
45
|
+
const method = field(params, "code_challenge_method");
|
|
46
|
+
if (!challenge || method !== "S256") {
|
|
47
|
+
redirectWithError(res, redirectUri, "invalid_request", "PKCE with code_challenge_method=S256 is required.", state);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
clientId,
|
|
52
|
+
redirectUri,
|
|
53
|
+
challenge,
|
|
54
|
+
state,
|
|
55
|
+
scope: optionalField(params, "scope")
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/** Consent granted: mint the bearer now, park it behind a one-shot code and send the browser back. Minting here
|
|
59
|
+
* rather than at redemption keeps a failure the user can act on — no space, revoked access — on this screen. */
|
|
60
|
+
var completeGrant = async (config, res, request, pending, target) => {
|
|
61
|
+
const issued = await config.adapters.issueToken(pending.user, target);
|
|
62
|
+
if (!issued) {
|
|
63
|
+
redirectWithError(res, request.redirectUri, "access_denied", "The account may not grant access to this resource.", request.state);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const code = randomId();
|
|
67
|
+
await putCode(config.adapters.store, code, {
|
|
68
|
+
clientId: request.clientId,
|
|
69
|
+
redirectUri: request.redirectUri,
|
|
70
|
+
challenge: request.challenge,
|
|
71
|
+
token: issued.token,
|
|
72
|
+
expiresInSeconds: issued.expiresInSeconds,
|
|
73
|
+
scope: request.scope,
|
|
74
|
+
user: pending.user,
|
|
75
|
+
target
|
|
76
|
+
}, config.codeTtlSeconds ?? DEFAULT_CODE_TTL_SECONDS);
|
|
77
|
+
redirectWithCode(res, request.redirectUri, code, request.state);
|
|
78
|
+
};
|
|
79
|
+
/** GET /authorize — the entry point a host opens in the user's browser. */
|
|
80
|
+
var handleAuthorizeStart = async (config, res, params) => {
|
|
81
|
+
const request = await resolveRequest(config, res, params);
|
|
82
|
+
if (!request) return;
|
|
83
|
+
await renderConsent(config, res, {
|
|
84
|
+
step: "credentials",
|
|
85
|
+
action: AUTHORIZE_PATH,
|
|
86
|
+
hidden: hiddenFieldsFor(request),
|
|
87
|
+
targets: [],
|
|
88
|
+
branding: config.branding ?? {}
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
/** POST /authorize — both steps of the form land here: the credentials submit, and the grant submit that carries
|
|
92
|
+
* the `pending` id proving the credentials step already passed. */
|
|
93
|
+
var handleAuthorizeSubmit = async (config, res, params) => {
|
|
94
|
+
const request = await resolveRequest(config, res, params);
|
|
95
|
+
if (!request) return;
|
|
96
|
+
const pendingId = optionalField(params, "pending");
|
|
97
|
+
if (pendingId) {
|
|
98
|
+
const pending = await getPending(config.adapters.store, pendingId);
|
|
99
|
+
if (!pending || pending.clientId !== request.clientId) {
|
|
100
|
+
redirectWithError(res, request.redirectUri, "access_denied", "The sign-in expired. Try connecting again.", request.state);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const targets = await config.adapters.grantTargets(pending.user);
|
|
104
|
+
const chosen = targets.find((target) => target.value === field(params, "target"));
|
|
105
|
+
if (!chosen) {
|
|
106
|
+
await renderConsent(config, res, {
|
|
107
|
+
step: "target",
|
|
108
|
+
action: AUTHORIZE_PATH,
|
|
109
|
+
hidden: hiddenFieldsFor(request, pendingId),
|
|
110
|
+
targets,
|
|
111
|
+
user: pending.user,
|
|
112
|
+
error: "Choose what to grant access to.",
|
|
113
|
+
branding: config.branding ?? {}
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await dropPending(config.adapters.store, pendingId);
|
|
118
|
+
await completeGrant(config, res, request, pending, chosen);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const user = await config.adapters.authenticate({
|
|
122
|
+
username: field(params, "username"),
|
|
123
|
+
password: field(params, "password")
|
|
124
|
+
});
|
|
125
|
+
if (!user) {
|
|
126
|
+
await renderConsent(config, res, {
|
|
127
|
+
step: "credentials",
|
|
128
|
+
action: AUTHORIZE_PATH,
|
|
129
|
+
hidden: hiddenFieldsFor(request),
|
|
130
|
+
targets: [],
|
|
131
|
+
error: "Those credentials did not match an account.",
|
|
132
|
+
branding: config.branding ?? {}
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const targets = await config.adapters.grantTargets(user);
|
|
137
|
+
if (targets.length === 0) {
|
|
138
|
+
redirectWithError(res, request.redirectUri, "access_denied", "This account has nothing to grant access to.", request.state);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const nextPendingId = randomId();
|
|
142
|
+
await putPending(config.adapters.store, nextPendingId, {
|
|
143
|
+
clientId: request.clientId,
|
|
144
|
+
redirectUri: request.redirectUri,
|
|
145
|
+
challenge: request.challenge,
|
|
146
|
+
state: request.state,
|
|
147
|
+
scope: request.scope,
|
|
148
|
+
user
|
|
149
|
+
});
|
|
150
|
+
await renderConsent(config, res, {
|
|
151
|
+
step: "target",
|
|
152
|
+
action: AUTHORIZE_PATH,
|
|
153
|
+
hidden: hiddenFieldsFor(request, nextPendingId),
|
|
154
|
+
targets,
|
|
155
|
+
user,
|
|
156
|
+
branding: config.branding ?? {}
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
//#endregion
|
|
160
|
+
export { handleAuthorizeStart, handleAuthorizeSubmit };
|