@plitzi/sdk-server 0.32.9 → 0.32.11
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 +45 -9
- package/dist/core/services/mcp.js +9 -7
- package/dist/helpers/serverLog.js +33 -0
- package/dist/index.js +2 -1
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +7 -0
- package/dist/modules/mcp/apps/example/view.tsx +70 -0
- package/dist/modules/mcp/apps/index.js +12 -0
- package/dist/modules/mcp/apps/render/index.js +16 -0
- package/dist/modules/mcp/apps/render/view.tsx +108 -0
- package/dist/modules/mcp/apps/shared/bundle.js +22 -0
- package/dist/modules/mcp/apps/shared/page.js +23 -0
- package/dist/modules/mcp/apps/shared/registerApp.js +27 -0
- package/dist/modules/mcp/apps/shared/resolve.js +5 -0
- package/dist/modules/mcp/apps/shared/shell.ejs +37 -0
- package/dist/modules/mcp/apps/shared/zodEnglishOnly.js +23 -0
- package/dist/modules/mcp/handler.js +8 -6
- package/dist/modules/mcp/helpers/log.js +20 -10
- package/dist/modules/mcp/resources/register.js +11 -5
- package/dist/modules/mcp/resources/renderGuide.js +17 -7
- package/dist/modules/mcp/server.js +21 -12
- package/dist/modules/mcp/tools/apply/index.js +1 -1
- package/dist/modules/mcp/tools/render.js +3 -3
- package/dist/modules/ssr/registerExternalPlugins.js +1 -1
- package/dist/modules/ssr/template.js +1 -1
- package/dist/plugins/compile.js +1 -1
- package/dist/plugins/copy.js +1 -1
- package/dist/plugins/manager.js +3 -1
- 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 +3 -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 +2 -1
- package/dist/src/mcp.d.ts +14 -0
- package/dist/src/modules/mcp/apps/apps.test.d.ts +1 -0
- package/dist/src/modules/mcp/apps/example/index.d.ts +8 -0
- package/dist/src/modules/mcp/apps/example/view.d.ts +1 -0
- package/dist/src/modules/mcp/apps/index.d.ts +8 -0
- package/dist/src/modules/mcp/apps/render/index.d.ts +3 -0
- package/dist/src/modules/mcp/apps/render/view.d.ts +1 -0
- package/dist/src/modules/mcp/apps/shared/bundle.d.ts +1 -0
- package/dist/src/modules/mcp/apps/shared/index.d.ts +2 -0
- package/dist/src/modules/mcp/apps/shared/page.d.ts +2 -0
- package/dist/src/modules/mcp/apps/shared/registerApp.d.ts +5 -0
- package/dist/src/modules/mcp/apps/shared/resolve.d.ts +1 -0
- package/dist/src/modules/mcp/apps/shared/zodEnglishOnly.d.ts +4 -0
- package/dist/src/modules/mcp/e2e/connector.test.d.ts +1 -0
- package/dist/src/modules/mcp/e2e/host.test.d.ts +1 -0
- package/dist/src/modules/mcp/e2e/index.d.ts +6 -0
- package/dist/src/modules/mcp/e2e/mcpEndpoint.d.ts +20 -0
- package/dist/src/modules/mcp/e2e/postMessageChannel.d.ts +22 -0
- package/dist/src/modules/mcp/e2e/renderingHost.d.ts +14 -0
- package/dist/src/modules/mcp/handler.d.ts +11 -7
- package/dist/src/modules/mcp/helpers/log.d.ts +3 -3
- package/dist/src/modules/mcp/index.d.ts +1 -0
- package/dist/src/modules/mcp/resources/register.d.ts +1 -1
- package/dist/src/modules/mcp/resources/renderGuide.d.ts +3 -2
- package/dist/src/modules/mcp/server.d.ts +4 -12
- package/dist/src/modules/mcp/tools/apply/index.d.ts +3 -3
- package/dist/src/modules/mcp/tools/operations/index.d.ts +6 -6
- package/dist/src/modules/mcp/tools/operations/schema/operations.d.ts +1 -1
- package/dist/src/modules/mcp/tools/operations/schema/settings/patchSettings.d.ts +1 -1
- package/dist/src/modules/mcp/tools/operations/style/operations.d.ts +2 -2
- package/dist/src/modules/mcp/tools/operations/style/shared.d.ts +1 -1
- package/dist/src/modules/mcp/tools/operations/style/variables/deleteStyleVariable.d.ts +1 -1
- package/dist/src/modules/mcp/tools/operations/style/variables/upsertStyleVariable.d.ts +1 -1
- package/dist/src/modules/mcp/tools/preview.d.ts +3 -3
- package/dist/src/modules/mcp/tools/render.d.ts +3 -3
- package/dist/src/modules/mcp/tools/screenshot.d.ts +3 -3
- package/dist/src/modules/mcp/tools/shared/tool.d.ts +4 -7
- package/dist/src/modules/mcp/tools/validate.d.ts +3 -3
- package/dist/src/modules/mcp/types/appTypes.d.ts +13 -0
- package/dist/src/modules/mcp/types/index.d.ts +1 -0
- package/dist/src/plugins/validate.d.ts +9 -0
- package/dist/src/plugins/validate.test.d.ts +1 -0
- package/package.json +13 -4
- package/dist/modules/mcp/resources/renderApp.js +0 -182
- package/dist/server.d.ts +0 -1
- package/dist/server.js +0 -107
- package/dist/src/modules/mcp/resources/renderApp.d.ts +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @plitzi/sdk-server
|
|
2
2
|
|
|
3
|
+
## 0.32.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- v0.32.11
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @plitzi/plitzi-sdk@0.32.11
|
|
10
|
+
- @plitzi/sdk-schema@0.32.11
|
|
11
|
+
- @plitzi/sdk-shared@0.32.11
|
|
12
|
+
|
|
13
|
+
## 0.32.10
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- v0.32.10
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
- @plitzi/plitzi-sdk@0.32.10
|
|
20
|
+
- @plitzi/sdk-schema@0.32.10
|
|
21
|
+
- @plitzi/sdk-shared@0.32.10
|
|
22
|
+
|
|
3
23
|
## 0.32.9
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
|
@@ -2,21 +2,57 @@ import { applySecurityHeaders } from "./securityHeaders.js";
|
|
|
2
2
|
import { buildResponseHelpers } from "../../helpers/buildResponseHelpers.js";
|
|
3
3
|
import { 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 logRequest = (error) => {
|
|
20
|
+
if (!logger) return;
|
|
21
|
+
const status = statusOf(rawRes, res);
|
|
22
|
+
logger({
|
|
23
|
+
kind: "request",
|
|
24
|
+
server,
|
|
25
|
+
method: req.method,
|
|
26
|
+
path: safePath(req.url),
|
|
27
|
+
...ctx.operation === void 0 ? {} : { operation: ctx.operation },
|
|
28
|
+
status,
|
|
29
|
+
durationMs: Date.now() - startedAt,
|
|
30
|
+
ok: !error && status < 400,
|
|
31
|
+
...error ? { error: errorText(error) } : {},
|
|
32
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
if (req.path === "\0") {
|
|
37
|
+
res.setStatus(400);
|
|
38
|
+
res.end();
|
|
39
|
+
logRequest();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
applySecurityHeaders(res, ctx.config, ctx.port);
|
|
43
|
+
for (const stage of stages) if (await stage(ctx)) {
|
|
44
|
+
logRequest();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
logRequest();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logRequest(error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
16
52
|
};
|
|
17
53
|
var makeHandler = (label, buildContext, stages) => {
|
|
18
54
|
return (raw, rawRes) => {
|
|
19
|
-
runPipeline(raw, rawRes, buildContext, stages).catch((err) => {
|
|
55
|
+
runPipeline(raw, rawRes, buildContext, stages, label).catch((err) => {
|
|
20
56
|
console.error(`[${label}] Unhandled error:`, err);
|
|
21
57
|
try {
|
|
22
58
|
if (!rawRes.headersSent) rawRes.writeHead(500, { "Content-Type": "text/plain" });
|
|
@@ -2,15 +2,17 @@ import { handleMcp } from "../../modules/mcp/handler.js";
|
|
|
2
2
|
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
|
-
var mcpPathOf = (
|
|
6
|
-
var serveMcp = (ctx) => {
|
|
7
|
-
const { previewClient, screenshot } = ctx.config;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
var mcpPathOf = (config) => config.mcpAi?.path ?? config.mcp?.path ?? "/mcp";
|
|
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
|
+
preview: previewClient ? createHttpPreviewClient(previewClient) : void 0,
|
|
10
|
+
screenshot: screenshot ? createHttpScreenshotClient(screenshot) : void 0,
|
|
11
|
+
logger
|
|
12
|
+
});
|
|
11
13
|
};
|
|
12
14
|
var mcpStage = async (ctx) => {
|
|
13
|
-
if (!ctx.req.path.startsWith(mcpPathOf(ctx.config
|
|
15
|
+
if (!ctx.req.path.startsWith(mcpPathOf(ctx.config))) return false;
|
|
14
16
|
await serveMcp(ctx);
|
|
15
17
|
return true;
|
|
16
18
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/helpers/serverLog.ts
|
|
2
|
+
var outcomeOf = (event) => event.ok ? "ok" : `ERROR ${event.error ?? ""}`.trim();
|
|
3
|
+
var renderRequest = (event) => {
|
|
4
|
+
const operation = event.operation ? ` ${event.operation}` : "";
|
|
5
|
+
const timing = `${Math.round(event.durationMs)}ms`;
|
|
6
|
+
return `[${event.server}] ${event.method} ${event.path}${operation} ${event.status} ${timing} ${outcomeOf(event)}`;
|
|
7
|
+
};
|
|
8
|
+
var renderTool = (event) => {
|
|
9
|
+
const args = event.argsSummary ? ` ${event.argsSummary}` : "";
|
|
10
|
+
return `[mcp] tools/call ${event.name}${args} ${Math.round(event.durationMs)}ms ${outcomeOf(event)}`;
|
|
11
|
+
};
|
|
12
|
+
var renderResource = (event) => `[mcp] resources/read ${event.name} ${Math.round(event.durationMs)}ms ${outcomeOf(event)}`;
|
|
13
|
+
/** One line for any {@link ServerLogEvent}: an HTTP request reads as an access-log line
|
|
14
|
+
* (`[SSR] GET /pricing 200 12ms ok`), the MCP events as what happened inside one
|
|
15
|
+
* (`[mcp] tools/call plitzi_apply {operations:[3]} 41ms ok`). Events arrive PII-free — the dispatcher strips
|
|
16
|
+
* query values and collects no headers, cookies or IPs, and tool args are summarised by shape — so rendering is
|
|
17
|
+
* a pure format. */
|
|
18
|
+
var renderLogEvent = (event) => {
|
|
19
|
+
switch (event.kind) {
|
|
20
|
+
case "request": return renderRequest(event);
|
|
21
|
+
case "tool": return renderTool(event);
|
|
22
|
+
case "resource": return renderResource(event);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** A drop-in `SSRServerConfig.logger` for consumers that just want the log on the console. Consumers with their
|
|
26
|
+
* own logging stack should pass their own sink instead and read the structured event. */
|
|
27
|
+
var consoleLogger = (event) => {
|
|
28
|
+
const line = renderLogEvent(event);
|
|
29
|
+
if (event.ok) console.log(line);
|
|
30
|
+
else console.error(line);
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
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.d.ts
ADDED
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { consoleLogger, renderLogEvent } from "./helpers/serverLog.js";
|
|
2
|
+
import { createMcpServer } from "./modules/mcp/server.js";
|
|
3
|
+
import { handleMcp, readMcpBody, serveMcp } from "./modules/mcp/handler.js";
|
|
4
|
+
import { createHttpPreviewClient } from "./modules/mcp/previewClient.js";
|
|
5
|
+
import { createHttpScreenshotClient } from "./modules/mcp/screenshotClient.js";
|
|
6
|
+
import { createMCPServer } from "./core/server/mcpServer.js";
|
|
7
|
+
export { consoleLogger, createHttpPreviewClient, createHttpScreenshotClient, createMCPServer, createMcpServer, handleMcp, readMcpBody, renderLogEvent, serveMcp };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* eslint-disable react-refresh/only-export-components -- one bundled entry: components cannot move out. */
|
|
2
|
+
import { useApp, useHostStyles } from '@modelcontextprotocol/ext-apps/react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { createRoot } from 'react-dom/client';
|
|
5
|
+
|
|
6
|
+
import type { McpUiHostContext } from '@modelcontextprotocol/ext-apps';
|
|
7
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
|
|
9
|
+
/** The smallest MCP App view there is — copy this folder to start a new one. It shows the whole contract:
|
|
10
|
+
* connect, receive the tool call and its result, call a tool back, and inherit the host's look. Everything it
|
|
11
|
+
* imports is bundled into the page, so a view that stays this small stays a few KB. */
|
|
12
|
+
|
|
13
|
+
// Host CSS variables: the chat's own palette and fonts, applied by useHostStyles below.
|
|
14
|
+
const panel = {
|
|
15
|
+
padding: 16,
|
|
16
|
+
font: 'var(--font-sans, system-ui, sans-serif)',
|
|
17
|
+
color: 'var(--color-text-primary, #0f172a)',
|
|
18
|
+
background: 'var(--color-background-secondary, #f8fafc)',
|
|
19
|
+
borderRadius: 'var(--border-radius-md, 8px)'
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
const textOf = (result: CallToolResult): string => result.content.find(entry => entry.type === 'text')?.text ?? '';
|
|
23
|
+
|
|
24
|
+
const ExampleApp = () => {
|
|
25
|
+
const [result, setResult] = useState<CallToolResult | null>(null);
|
|
26
|
+
const [context, setContext] = useState<McpUiHostContext | undefined>(undefined);
|
|
27
|
+
|
|
28
|
+
// useApp creates the App, runs onAppCreated so every handler is in place BEFORE the handshake, and connects.
|
|
29
|
+
const { app, error } = useApp({
|
|
30
|
+
appInfo: { name: 'Plitzi Example', version: '1.0.0' },
|
|
31
|
+
capabilities: {},
|
|
32
|
+
onAppCreated: instance => {
|
|
33
|
+
// The result of the tool that opened this view. `ontoolinput` gives its arguments instead.
|
|
34
|
+
instance.ontoolresult = setResult;
|
|
35
|
+
instance.onhostcontextchanged = params => setContext(previous => ({ ...previous, ...params }));
|
|
36
|
+
instance.onteardown = () => ({});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
useHostStyles(app, context);
|
|
41
|
+
|
|
42
|
+
if (error) {
|
|
43
|
+
return <div style={panel}>Could not connect to the host: {error.message}</div>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!app) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const insets = context?.safeAreaInsets;
|
|
51
|
+
const message = result ? textOf(result) : 'Waiting for the tool result…';
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div style={{ ...panel, margin: `${insets?.top ?? 0}px ${insets?.right ?? 0}px` }}>
|
|
55
|
+
<p>{message}</p>
|
|
56
|
+
{/* An app can call the server's tools on its own — each call is a round-trip, so plan for latency. */}
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => void app.callServerTool({ name: 'plitzi_search', arguments: {} }).then(setResult)}
|
|
60
|
+
>
|
|
61
|
+
Call plitzi_search
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const root = document.getElementById('app');
|
|
68
|
+
if (root) {
|
|
69
|
+
createRoot(root).render(<ExampleApp />);
|
|
70
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { registerApp } from "./shared/registerApp.js";
|
|
2
|
+
import { RENDER_APP_URI, renderApp } from "./render/index.js";
|
|
3
|
+
//#region src/modules/mcp/apps/index.ts
|
|
4
|
+
/** Every MCP App this server serves. A new one is a folder beside `render/` (its definition + its view) and one
|
|
5
|
+
* line here — the tool that opens it points at its `uri` through `ui: { resourceUri }`. Start from `example/`,
|
|
6
|
+
* which is that same shape stripped to the minimum. */
|
|
7
|
+
var apps = [renderApp];
|
|
8
|
+
var registerApps = (server) => {
|
|
9
|
+
for (const app of apps) registerApp(server, app);
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { RENDER_APP_URI, apps, registerApps };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { require } from "../shared/resolve.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
//#region src/modules/mcp/apps/render/index.ts
|
|
5
|
+
var RENDER_APP_URI = "ui://plitzi/render.html";
|
|
6
|
+
var HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
var renderApp = {
|
|
8
|
+
uri: RENDER_APP_URI,
|
|
9
|
+
name: "plitzi-render-app",
|
|
10
|
+
title: "Plitzi widget",
|
|
11
|
+
description: "Interactive view that renders a plitzi_render widget with the Plitzi SDK.",
|
|
12
|
+
entry: path.join(HERE, "view.tsx"),
|
|
13
|
+
styles: () => [path.join(path.dirname(require.resolve("@plitzi/plitzi-sdk")), "plitzi-sdk.css")]
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { RENDER_APP_URI, renderApp };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* eslint-disable react-refresh/only-export-components -- one bundled entry: components cannot move out. */
|
|
2
|
+
import { useApp, useHostStyles } from '@modelcontextprotocol/ext-apps/react';
|
|
3
|
+
import PlitziSdk from '@plitzi/plitzi-sdk';
|
|
4
|
+
import { Component, useEffect, useState } from 'react';
|
|
5
|
+
import { createRoot } from 'react-dom/client';
|
|
6
|
+
|
|
7
|
+
import type { McpUiHostContext } from '@modelcontextprotocol/ext-apps';
|
|
8
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import type { OfflineDataRaw } from '@plitzi/sdk-shared';
|
|
10
|
+
import type { ReactNode } from 'react';
|
|
11
|
+
|
|
12
|
+
/** The view for plitzi_render: the entry the server bundles (React, the MCP Apps runtime and the Plitzi SDK
|
|
13
|
+
* included) and inlines in the shared ui:// page shell — which is what lets it run in a sandbox that can fetch
|
|
14
|
+
* nothing. It mounts on `#app`, the shell's root. */
|
|
15
|
+
|
|
16
|
+
const panelStyle = { padding: 16, font: '13px/1.5 system-ui, sans-serif', color: '#b91c1c' } as const;
|
|
17
|
+
|
|
18
|
+
// Every failure ends up here: an iframe left on its placeholder would hide the reason in the sandbox's console.
|
|
19
|
+
const ErrorPanel = ({ title, details }: { title: string; details: string }) => (
|
|
20
|
+
<div style={panelStyle}>
|
|
21
|
+
<strong>{title}</strong>
|
|
22
|
+
<pre style={{ margin: '8px 0 0', whiteSpace: 'pre-wrap', color: '#7f1d1d', fontSize: 12 }}>{details}</pre>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
class RenderBoundary extends Component<{ children: ReactNode }, { error?: Error }> {
|
|
27
|
+
state: { error?: Error } = {};
|
|
28
|
+
|
|
29
|
+
static getDerivedStateFromError(error: Error) {
|
|
30
|
+
return { error };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
render() {
|
|
34
|
+
if (this.state.error) {
|
|
35
|
+
return <ErrorPanel title="Widget failed to render" details={this.state.error.message} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return this.props.children;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const RenderApp = () => {
|
|
43
|
+
const [result, setResult] = useState<CallToolResult | null>(null);
|
|
44
|
+
const [cancelled, setCancelled] = useState<string | undefined>(undefined);
|
|
45
|
+
const [context, setContext] = useState<McpUiHostContext | undefined>(undefined);
|
|
46
|
+
|
|
47
|
+
// useApp creates the App, runs onAppCreated so every handler is in place BEFORE the handshake, and connects.
|
|
48
|
+
const { app, error } = useApp({
|
|
49
|
+
appInfo: { name: 'Plitzi Widget', version: '1.0.0' },
|
|
50
|
+
capabilities: {},
|
|
51
|
+
onAppCreated: instance => {
|
|
52
|
+
instance.ontoolresult = setResult;
|
|
53
|
+
instance.ontoolcancelled = params => setCancelled(params.reason ?? 'The host cancelled the render.');
|
|
54
|
+
instance.onhostcontextchanged = params => setContext(previous => ({ ...previous, ...params }));
|
|
55
|
+
instance.onteardown = () => ({});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// The context the host sent in its initialize result predates any change notification.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (app) {
|
|
62
|
+
setContext(app.getHostContext());
|
|
63
|
+
}
|
|
64
|
+
}, [app]);
|
|
65
|
+
|
|
66
|
+
useHostStyles(app, context);
|
|
67
|
+
|
|
68
|
+
const failure = error?.message ?? cancelled;
|
|
69
|
+
if (failure) {
|
|
70
|
+
return <ErrorPanel title="Could not render the widget" details={failure} />;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Still connecting, or connected and waiting for the result: the page's CSS placeholder covers both.
|
|
74
|
+
if (!result) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The payload rides in structuredContent, which the protocol types as unknown-valued; plitzi_render is the one
|
|
79
|
+
// guaranteeing the shape, and the SDK validates it again when it renders.
|
|
80
|
+
const offlineData = result.structuredContent?.offlineData as OfflineDataRaw | undefined;
|
|
81
|
+
if (!offlineData) {
|
|
82
|
+
// A failed render answers with its (already compact) reasons as the tool's JSON text summary.
|
|
83
|
+
const text = result.content.find(entry => entry.type === 'text');
|
|
84
|
+
|
|
85
|
+
return <ErrorPanel title="Render failed" details={text?.text ?? 'The tool returned no widget data.'} />;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const insets = context?.safeAreaInsets;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
style={{
|
|
93
|
+
padding: `${insets?.top ?? 0}px ${insets?.right ?? 0}px ${insets?.bottom ?? 0}px ${insets?.left ?? 0}px`
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<RenderBoundary>
|
|
97
|
+
<PlitziSdk offlineData={offlineData} offlineMode environment="main" renderMode="raw" />
|
|
98
|
+
</RenderBoundary>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const root = document.getElementById('app');
|
|
104
|
+
if (root) {
|
|
105
|
+
// The SDK styles its tree from this container class down.
|
|
106
|
+
root.className = 'plitzi-root-container';
|
|
107
|
+
createRoot(root).render(<RenderApp />);
|
|
108
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { require } from "./resolve.js";
|
|
2
|
+
import { zodEnglishOnly } from "./zodEnglishOnly.js";
|
|
3
|
+
import { build } from "esbuild";
|
|
4
|
+
//#region src/modules/mcp/apps/shared/bundle.ts
|
|
5
|
+
var bundle = async (entry) => {
|
|
6
|
+
return (await build({
|
|
7
|
+
entryPoints: [entry],
|
|
8
|
+
bundle: true,
|
|
9
|
+
write: false,
|
|
10
|
+
format: "iife",
|
|
11
|
+
platform: "browser",
|
|
12
|
+
target: "es2022",
|
|
13
|
+
jsx: "automatic",
|
|
14
|
+
minify: true,
|
|
15
|
+
alias: { "@plitzi/plitzi-sdk": require.resolve("@plitzi/plitzi-sdk") },
|
|
16
|
+
define: { "process.env.NODE_ENV": "\"production\"" },
|
|
17
|
+
plugins: [zodEnglishOnly],
|
|
18
|
+
logLevel: "silent"
|
|
19
|
+
})).outputFiles[0].text;
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { bundle };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { bundle } from "./bundle.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import ejs from "ejs";
|
|
6
|
+
//#region src/modules/mcp/apps/shared/page.ts
|
|
7
|
+
var SHELL = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "shell.ejs");
|
|
8
|
+
var template = () => ejs.compile(readFileSync(SHELL, "utf-8"), { filename: SHELL });
|
|
9
|
+
var pages = /* @__PURE__ */ new Map();
|
|
10
|
+
var page = (app) => {
|
|
11
|
+
let html = pages.get(app.uri);
|
|
12
|
+
if (!html) {
|
|
13
|
+
html = bundle(app.entry).then((script) => template()({
|
|
14
|
+
title: app.title,
|
|
15
|
+
app: script,
|
|
16
|
+
css: (app.styles?.() ?? []).map((file) => readFileSync(file, "utf-8")).join("\n")
|
|
17
|
+
}));
|
|
18
|
+
pages.set(app.uri, html);
|
|
19
|
+
}
|
|
20
|
+
return html;
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { page };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { page } from "./page.js";
|
|
2
|
+
import { RESOURCE_MIME_TYPE, registerAppResource } from "@modelcontextprotocol/ext-apps/server";
|
|
3
|
+
//#region src/modules/mcp/apps/shared/registerApp.ts
|
|
4
|
+
var DEFAULT_CSP = {
|
|
5
|
+
resourceDomains: [
|
|
6
|
+
"*",
|
|
7
|
+
"data:",
|
|
8
|
+
"blob:"
|
|
9
|
+
],
|
|
10
|
+
connectDomains: ["*"]
|
|
11
|
+
};
|
|
12
|
+
/** Serves the app as a self-contained page: no import map, no asset mounts, no cross-origin fetches, so the
|
|
13
|
+
* strictest host sandbox runs it and no deployment has to serve anything extra. */
|
|
14
|
+
var registerApp = (server, app) => {
|
|
15
|
+
const meta = { ui: { csp: app.csp ?? DEFAULT_CSP } };
|
|
16
|
+
registerAppResource(server, app.name, app.uri, {
|
|
17
|
+
description: app.description,
|
|
18
|
+
_meta: meta
|
|
19
|
+
}, async () => ({ contents: [{
|
|
20
|
+
uri: app.uri,
|
|
21
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
22
|
+
text: await page(app),
|
|
23
|
+
_meta: meta
|
|
24
|
+
}] }));
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { registerApp };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
The page shell every MCP App shares. The host renders it in a sandboxed iframe on ITS OWN origin: the app and
|
|
3
|
+
its styles are inlined, so the page fetches NOTHING and runs under a deny-by-default sandbox CSP.
|
|
4
|
+
-%>
|
|
5
|
+
<!doctype html>
|
|
6
|
+
<html lang="en">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="utf-8" />
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
10
|
+
<title><%= title %></title>
|
|
11
|
+
|
|
12
|
+
<style>
|
|
13
|
+
html,
|
|
14
|
+
body {
|
|
15
|
+
margin: 0;
|
|
16
|
+
}
|
|
17
|
+
/* The app renders nothing until the host pushes the tool result in. */
|
|
18
|
+
#app:empty::after {
|
|
19
|
+
content: 'Rendering…';
|
|
20
|
+
display: block;
|
|
21
|
+
padding: 16px;
|
|
22
|
+
font: 14px system-ui, sans-serif;
|
|
23
|
+
color: #64748b;
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
26
|
+
<style>
|
|
27
|
+
<%- css %>
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<div id="app"></div>
|
|
32
|
+
|
|
33
|
+
<script>
|
|
34
|
+
<%- app %>
|
|
35
|
+
</script>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/modules/mcp/apps/shared/zodEnglishOnly.ts
|
|
2
|
+
var NAMESPACE = "zod-locales-stub";
|
|
3
|
+
var ZOD_PACKAGE = /node_modules[/\\]zod[/\\]/u;
|
|
4
|
+
/** Drops zod's ~40 translations (194 KB): they are re-exported as a namespace, which no bundler can tree-shake.
|
|
5
|
+
* English survives because zod imports `locales/en.js` directly, not through this index. */
|
|
6
|
+
var zodEnglishOnly = {
|
|
7
|
+
name: "zod-english-only",
|
|
8
|
+
setup(builder) {
|
|
9
|
+
builder.onResolve({ filter: /locales[/\\]index\.js$/ }, (args) => ZOD_PACKAGE.test(args.importer) ? {
|
|
10
|
+
path: args.path,
|
|
11
|
+
namespace: NAMESPACE
|
|
12
|
+
} : void 0);
|
|
13
|
+
builder.onLoad({
|
|
14
|
+
filter: /.*/,
|
|
15
|
+
namespace: NAMESPACE
|
|
16
|
+
}, () => ({
|
|
17
|
+
contents: "export {};",
|
|
18
|
+
loader: "js"
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { zodEnglishOnly };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createMcpServer } from "./server.js";
|
|
2
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp";
|
|
2
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
3
|
//#region src/modules/mcp/handler.ts
|
|
4
4
|
var readMcpBody = (req) => new Promise((resolve, reject) => {
|
|
5
5
|
let raw = "";
|
|
@@ -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");
|
|
@@ -60,17 +64,15 @@ var serveMcp = async (raw, res, server) => {
|
|
|
60
64
|
return;
|
|
61
65
|
}
|
|
62
66
|
await transport.handleRequest(raw, res, body);
|
|
67
|
+
return operationOf(body);
|
|
63
68
|
} finally {
|
|
64
69
|
await transport.close();
|
|
65
70
|
}
|
|
66
71
|
};
|
|
67
|
-
var handleMcp = (raw, res, req, adapters,
|
|
72
|
+
var handleMcp = (raw, res, req, adapters, options = {}) => serveMcp(raw, res, createMcpServer({
|
|
68
73
|
adapters,
|
|
69
74
|
getSpaceId: () => adapters.getSpaceId?.(req) ?? Promise.resolve(void 0),
|
|
70
|
-
|
|
71
|
-
screenshot,
|
|
72
|
-
logger,
|
|
73
|
-
renderApp
|
|
75
|
+
...options
|
|
74
76
|
}));
|
|
75
77
|
//#endregion
|
|
76
78
|
export { createMcpServer, handleMcp, readMcpBody, serveMcp };
|
|
@@ -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({
|