@plitzi/sdk-server 0.32.16 → 0.32.17
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 +10 -0
- package/dist/helpers/serverLog.js +3 -3
- package/dist/modules/mcp/apps/shared/shell.ejs +19 -2
- package/dist/modules/mcp/helpers/space.js +1 -1
- package/dist/modules/mcp/resources/renderGuide.js +90 -10
- package/dist/modules/mcp/server.js +1 -1
- package/dist/modules/mcp/tools/render.js +1 -1
- package/dist/modules/oauth/metadata.js +11 -4
- package/dist/src/helpers/serverLog.d.ts +1 -1
- package/dist/src/modules/mcp/helpers/space.d.ts +1 -1
- package/dist/src/modules/oauth/metadata.d.ts +11 -4
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -8,12 +8,12 @@ var renderRequest = (event) => {
|
|
|
8
8
|
};
|
|
9
9
|
var renderTool = (event) => {
|
|
10
10
|
const args = event.argsSummary ? ` ${event.argsSummary}` : "";
|
|
11
|
-
return `[
|
|
11
|
+
return `[MCP] tools/call ${event.name}${args} ${Math.round(event.durationMs)}ms ${outcomeOf(event)}`;
|
|
12
12
|
};
|
|
13
|
-
var renderResource = (event) => `[
|
|
13
|
+
var renderResource = (event) => `[MCP] resources/read ${event.name} ${Math.round(event.durationMs)}ms ${outcomeOf(event)}`;
|
|
14
14
|
/** One line for any {@link ServerLogEvent}: an HTTP request reads as an access-log line
|
|
15
15
|
* (`[SSR] 203.0.113.7 GET /pricing 200 12ms ok`), the MCP events as what happened inside one
|
|
16
|
-
* (`[
|
|
16
|
+
* (`[MCP] tools/call plitzi_apply {operations:[3]} 41ms ok`). Rendering is a pure format — the dispatcher
|
|
17
17
|
* already stripped query values, collected no headers, cookies or tokens and summarised tool args by shape;
|
|
18
18
|
* the client IP it does carry is personal data, so a sink that persists these lines must say so. */
|
|
19
19
|
var renderLogEvent = (event) => {
|
|
@@ -10,17 +10,34 @@
|
|
|
10
10
|
<title><%= title %></title>
|
|
11
11
|
|
|
12
12
|
<style>
|
|
13
|
+
/*
|
|
14
|
+
The page inherits the HOST's theme, which may be dark: nothing here paints a surface, so an app that sets no
|
|
15
|
+
colours would otherwise draw the browser's black default text over the host's own background — unreadable in
|
|
16
|
+
a dark chat. `light-dark()` picks the right side of every pair; `color-scheme` is what tells it which side,
|
|
17
|
+
and it follows the OS until useHostStyles replaces it with the theme the host actually reported (that hook
|
|
18
|
+
also fills the --color-* variables in, so the var() fallbacks below only apply on a host that sends none).
|
|
19
|
+
*/
|
|
20
|
+
html {
|
|
21
|
+
color-scheme: light dark;
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
html,
|
|
14
25
|
body {
|
|
15
26
|
margin: 0;
|
|
16
27
|
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
background-color: transparent;
|
|
31
|
+
color: var(--color-text-primary, light-dark(#0f172a, #e8eaed));
|
|
32
|
+
font-family: var(--font-sans, system-ui, sans-serif);
|
|
33
|
+
}
|
|
17
34
|
/* The app renders nothing until the host pushes the tool result in. */
|
|
18
35
|
#app:empty::after {
|
|
19
36
|
content: 'Rendering…';
|
|
20
37
|
display: block;
|
|
21
38
|
padding: 16px;
|
|
22
|
-
font: 14px
|
|
23
|
-
color: #64748b;
|
|
39
|
+
font-size: 14px;
|
|
40
|
+
color: var(--color-text-tertiary, light-dark(#64748b, #9aa4b2));
|
|
24
41
|
}
|
|
25
42
|
</style>
|
|
26
43
|
<style>
|
|
@@ -280,7 +280,7 @@ var pageRefOfElement = (schema, el) => {
|
|
|
280
280
|
/** Total number of descendant elements under a subtree (excluding the root). */
|
|
281
281
|
var descendantCount = (schema, rootId) => descendantIds(schema, rootId).length;
|
|
282
282
|
var emptySpaceMessage = "Space data not available";
|
|
283
|
-
var unauthorizedSpaceMessage = "This
|
|
283
|
+
var unauthorizedSpaceMessage = "This connection has no space attached (a guest or widgets-only grant, or a token that carries no space), so NOTHING in a space can be read or edited — every other space tool will fail the same way, do not retry them. Use plitzi_render instead: it builds a self-contained widget offline, with no space, backend or account (read plitzi://render/guide). To edit a real space, the user must reconnect the integration and grant access to one.";
|
|
284
284
|
var generateObjectId = () => {
|
|
285
285
|
return `${Math.floor(Date.now() / 1e3).toString(16).padStart(8, "0")}${Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join("")}`;
|
|
286
286
|
};
|
|
@@ -68,7 +68,21 @@ Styling is separate from structure: declare a class, then attach it by ref.
|
|
|
68
68
|
- CSS properties in **kebab-case** (\`background-color\`, \`font-size\`, \`border-radius\`), values as plain strings.
|
|
69
69
|
- Attach to an element via \`style: { "base": ["card"] }\`. Stack classes: \`"base": ["card", "shadow"]\`.
|
|
70
70
|
- One \`ref\` can name both an element and its class (as above) — they live in different namespaces.
|
|
71
|
-
- Lay containers out with flexbox
|
|
71
|
+
- Lay containers out with flexbox or grid — pick the direction on purpose, see **Fit the panel** below.
|
|
72
|
+
- **You are not styling from zero.** Each type lands on the page with CSS you did not write, and it is the usual
|
|
73
|
+
reason a widget does not look like the definitions say. The SDK stylesheet resets almost nothing (\`box-sizing\`,
|
|
74
|
+
\`border: 0 solid\`, \`body\` margin), so anything you leave unset comes from one of two places:
|
|
75
|
+
- The per-type rule the SDK ships — the one that changes layouts is \`container\`, which carries
|
|
76
|
+
\`min-width: 50px; min-height: 50px\`. A rail, a divider, a dot, a spacer or a narrow cell will NOT go below
|
|
77
|
+
50px until you say \`"min-width": "0"\` (and/or \`"min-height": "0"\`) on it: a 2px timeline line renders 50px
|
|
78
|
+
wide otherwise. Any flex child that must be allowed to shrink needs it too.
|
|
79
|
+
- The BROWSER's own defaults for the rest: \`heading\` keeps its UA font-size and ~0.67em top/bottom margins,
|
|
80
|
+
\`paragraph\` ~1em margins, \`list\` a 40px \`padding-left\`, \`button\` its native chrome, \`image\` its intrinsic
|
|
81
|
+
size, \`link\` its own colour and underline. In a compact widget set these explicitly — usually
|
|
82
|
+
\`"margin-top": "0"\`, \`"margin-bottom": "0"\` — and space things with the parent's \`gap\` instead.
|
|
83
|
+
- Borders start at \`0 solid\`, so \`border-color\` alone paints nothing: give \`border-width\` (and the colour).
|
|
84
|
+
- The \`defaultStyle\` a type reports in the EDITING catalog (\`plitzi://types\`) is authoring metadata for the
|
|
85
|
+
builder — it does not paint here, so do not count on it in a widget.
|
|
72
86
|
- **Mind the intrinsic display.** Some types start non-block: \`text\` is \`display: inline\`, so to stack or size it,
|
|
73
87
|
wrap it in a \`container\` (or set \`display: block\`). \`heading\` and \`paragraph\` are already block.
|
|
74
88
|
- **Use atomic longhands.** \`padding\`, \`margin\`, \`border\`, \`border-radius\` are fine (they expand cleanly), but
|
|
@@ -80,6 +94,62 @@ Styling is separate from structure: declare a class, then attach it by ref.
|
|
|
80
94
|
\`{ "desktop": { "background-color": "#3b82f6" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } }\`
|
|
81
95
|
(\`hover\`, \`active\`, \`focus\`).
|
|
82
96
|
|
|
97
|
+
## Fit the panel — go wide, stay short
|
|
98
|
+
|
|
99
|
+
The widget renders in a **side panel** (Claude Desktop, ChatGPT, the Plitzi builder), so it gets a usable width but
|
|
100
|
+
very little height: everything past the first screenful costs the user a scroll. Height is the scarce resource —
|
|
101
|
+
spend width instead.
|
|
102
|
+
|
|
103
|
+
Plain containers are blocks, so doing nothing stacks children **vertically** and produces exactly the tall,
|
|
104
|
+
half-empty widget to avoid. Choose the axis every time:
|
|
105
|
+
|
|
106
|
+
- **Peers side by side** — metrics, plans, options, a comparison, an image next to its text: a row, wrapping when
|
|
107
|
+
it runs out of width, children sharing it (no fixed widths).
|
|
108
|
+
\`\`\`json
|
|
109
|
+
{ "type": "upsertDefinition", "ref": "row", "desktop": { "display": "flex", "flex-direction": "row", "flex-wrap": "wrap", "gap": "12px", "align-items": "stretch" } }
|
|
110
|
+
{ "type": "upsertDefinition", "ref": "col", "desktop": { "flex-grow": "1", "flex-basis": "0%", "min-width": "150px" } }
|
|
111
|
+
\`\`\`
|
|
112
|
+
\`flex-grow: 1\` + \`flex-basis: 0%\` splits the row evenly; \`min-width\` is the wrap threshold — under it the item
|
|
113
|
+
drops to the next line by itself, so a narrow panel degrades gracefully with no breakpoints.
|
|
114
|
+
- **Many uniform items** — cards, tiles, a gallery: one grid line does it all.
|
|
115
|
+
\`\`\`json
|
|
116
|
+
{ "type": "upsertDefinition", "ref": "grid", "desktop": { "display": "grid", "grid-template-columns": "repeat(auto-fit, minmax(160px, 1fr))", "gap": "12px" } }
|
|
117
|
+
\`\`\`
|
|
118
|
+
- **Label + value pairs** stay on one line (\`display: flex\`, \`justify-content: space-between\`) instead of two.
|
|
119
|
+
- **Vertical is right** for reading order: a heading over its paragraph, a form, a step list, long prose.
|
|
120
|
+
- Force a stack on tiny screens with the \`mobile\` block: \`"mobile": { "flex-direction": "column" }\`.
|
|
121
|
+
|
|
122
|
+
Keep it compact, and the numbers low: \`padding\` 12–16px (24+ only on a single hero card), \`gap\` 8–12px,
|
|
123
|
+
\`font-size\` 13–15px for body and 16–20px for headings. Let the outer container **fill** the panel — no \`width\` on
|
|
124
|
+
it — and reach for \`max-width\` only to stop one lone card from stretching across the whole panel.
|
|
125
|
+
|
|
126
|
+
## Match the host theme — never hardcode a light palette
|
|
127
|
+
|
|
128
|
+
The widget is embedded in the host's own UI (Claude Desktop, ChatGPT, the builder), and that UI **may be in dark
|
|
129
|
+
mode**. A widget painted with fixed light colours is the most common way to ship something unusable: a white card
|
|
130
|
+
in a dark chat glares, and — worse — text left at a dark default disappears against the host's dark background.
|
|
131
|
+
|
|
132
|
+
The host publishes its palette as CSS variables on the page, so use them for every colour, with a
|
|
133
|
+
\`light-dark(<light>, <dark>)\` fallback for hosts that send none:
|
|
134
|
+
|
|
135
|
+
\`\`\`json
|
|
136
|
+
{ "type": "upsertDefinition", "ref": "card", "desktop": {
|
|
137
|
+
"background-color": "var(--color-background-secondary, light-dark(#ffffff, #1f2430))",
|
|
138
|
+
"color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))",
|
|
139
|
+
"border-width": "1px", "border-style": "solid",
|
|
140
|
+
"border-color": "var(--color-border-primary, light-dark(#e2e8f0, #333a48))"
|
|
141
|
+
} }
|
|
142
|
+
\`\`\`
|
|
143
|
+
|
|
144
|
+
- Surfaces: \`--color-background-primary\` (the page), \`--color-background-secondary\` / \`--color-background-tertiary\`
|
|
145
|
+
(cards, raised areas). Text: \`--color-text-primary\`, \`--color-text-secondary\`, \`--color-text-tertiary\` (muted).
|
|
146
|
+
Borders: \`--color-border-primary\` / \`--color-border-secondary\`. Status pairs: \`--color-background-danger\` |
|
|
147
|
+
\`success\` | \`warning\` | \`info\` with the matching \`--color-text-…\`. Also \`--font-sans\`, \`--border-radius-md\` /
|
|
148
|
+
\`lg\` / \`full\`, \`--shadow-sm\` / \`md\`.
|
|
149
|
+
- **Set \`color\` wherever you set \`background-color\`** — the pair is what stays legible, either one alone is a
|
|
150
|
+
gamble. Same rule for a brand accent: a CTA on your own blue must state its own text colour (\`#ffffff\`).
|
|
151
|
+
- Prefer a border to a drop shadow for separation: shadows all but vanish on a dark surface.
|
|
152
|
+
|
|
83
153
|
## Element types (type → what to set)
|
|
84
154
|
|
|
85
155
|
| type | renders | set |
|
|
@@ -103,21 +173,31 @@ that is not here (lists, tabs, dialogs, forms, icons…).
|
|
|
103
173
|
\`image\`/\`video\` \`src\` accepts any \`https\` URL, or a \`data:\`/\`blob:\` URI for a fully self-contained graphic
|
|
104
174
|
(e.g. an inline SVG icon or a base64 image) — both render with no extra setup.
|
|
105
175
|
|
|
106
|
-
## Full worked example —
|
|
176
|
+
## Full worked example — two plans side by side
|
|
177
|
+
|
|
178
|
+
The cards sit in a wrapping row and split it evenly, so the widget uses the panel's width and stays short; on a
|
|
179
|
+
narrow panel \`min-width\` drops the second card under the first on its own.
|
|
107
180
|
|
|
108
181
|
\`\`\`json
|
|
109
182
|
{
|
|
110
183
|
"operations": [
|
|
111
|
-
{ "type": "upsertDefinition", "ref": "
|
|
112
|
-
{ "type": "upsertDefinition", "ref": "
|
|
113
|
-
{ "type": "upsertDefinition", "ref": "
|
|
184
|
+
{ "type": "upsertDefinition", "ref": "plans", "desktop": { "display": "flex", "flex-direction": "row", "flex-wrap": "wrap", "gap": "12px", "align-items": "stretch" } },
|
|
185
|
+
{ "type": "upsertDefinition", "ref": "card", "desktop": { "display": "flex", "flex-direction": "column", "gap": "6px", "flex-grow": "1", "flex-basis": "0%", "min-width": "150px", "padding": "16px", "background-color": "var(--color-background-secondary, light-dark(#ffffff, #1f2430))", "color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))", "border-width": "1px", "border-style": "solid", "border-color": "var(--color-border-primary, light-dark(#e2e8f0, #333a48))", "border-radius": "var(--border-radius-lg, 12px)", "text-align": "center" } },
|
|
186
|
+
{ "type": "upsertDefinition", "ref": "price", "desktop": { "font-size": "28px", "font-weight": "800", "color": "#3b82f6" } },
|
|
187
|
+
{ "type": "upsertDefinition", "ref": "cta", "desktop": { "background-color": "#3b82f6", "color": "#ffffff", "padding": "10px 16px", "border-radius": "8px", "font-weight": "600" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } },
|
|
114
188
|
{ "type": "upsertElement", "pageRef": "render", "element": {
|
|
115
|
-
"ref": "
|
|
189
|
+
"ref": "plans", "type": "container", "style": { "base": ["plans"] },
|
|
116
190
|
"children": [
|
|
117
|
-
{ "ref": "
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
191
|
+
{ "ref": "free", "type": "container", "style": { "base": ["card"] }, "children": [
|
|
192
|
+
{ "ref": "free-plan", "type": "heading", "subType": "h3", "props": { "content": "Starter" } },
|
|
193
|
+
{ "ref": "free-amount", "type": "text", "props": { "content": "$0" }, "style": { "base": ["price"] } },
|
|
194
|
+
{ "ref": "free-buy", "type": "button", "props": { "content": "Start free" }, "style": { "base": ["cta"] } }
|
|
195
|
+
] },
|
|
196
|
+
{ "ref": "pro", "type": "container", "style": { "base": ["card"] }, "children": [
|
|
197
|
+
{ "ref": "pro-plan", "type": "heading", "subType": "h3", "props": { "content": "Pro" } },
|
|
198
|
+
{ "ref": "pro-amount", "type": "text", "props": { "content": "$29/mo" }, "style": { "base": ["price"] } },
|
|
199
|
+
{ "ref": "pro-buy", "type": "button", "props": { "content": "Start free trial" }, "style": { "base": ["cta"] } }
|
|
200
|
+
] }
|
|
121
201
|
]
|
|
122
202
|
} }
|
|
123
203
|
]
|
|
@@ -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.17"
|
|
47
47
|
}, { instructions: serverInstructions });
|
|
48
48
|
registerResources(server, getSpace, MCP_ENV, log);
|
|
49
49
|
registerApps(server);
|
|
@@ -105,7 +105,7 @@ var toRenderResult = (res) => {
|
|
|
105
105
|
var renderTool = defineTool({
|
|
106
106
|
name: "plitzi_render",
|
|
107
107
|
title: "Render widget",
|
|
108
|
-
description: "Show the user a real, rendered UI widget instead of describing one — cards, hero sections, pricing tables, forms, menus, checklists, profiles, galleries. It runs the Plitzi SDK fully offline: no backend, account, or setup. Reach for it whenever a visual layout beats prose: the user asks you to design/build/show something, OR your answer is naturally visual (a recipe → a card, a comparison → a table, steps → a checklist). Prefer showing over telling.\n\nAuthor the widget as an ordered list of `operations` that build an element tree under the pre-seeded root page \"render\". Three rules:\n1. STRUCTURE — one upsertElement builds the whole tree: set pageRef:\"render\" and give element a nested `children` array. Each element is { ref (unique), type, subType?, props?, style?, children? }; children render in order. (To attach to something you already made, use a top-level parentRef:\"<existing ref>\" instead.)\n2. STYLE — declare reusable classes with upsertDefinition { ref, desktop:{ …CSS props in kebab-case… } }, then attach via the element style:{ base:[\"<class ref>\"] }. Lay containers out with flex/grid.\n3. CONTENT — visible copy goes in props.content (text, heading, paragraph, button); heading level is the element subType (\"h1\"..\"h6\"); image/video take props.src. An unknown prop comes back as a warning naming the right one.\n\nCommon types: container, heading, paragraph, text, button, link, image, video, list, listItem, markdown (plitzi://render/types lists every built-in type with descriptions). Widgets can also be data-driven and interactive — an apiContainer fetches at runtime, upsertBinding wires data into elements, and upsertInteractionFlow makes them react to clicks (see the guide).\nREAD the resource plitzi://render/guide first — it has the element/prop table, the style model and a full worked example, and following it is the difference between a widget that renders and repeated failed calls.\nReturns a compact summary (the widget is shown to the user); on failure it returns teachable errors (path + hint) — read them and retry.",
|
|
108
|
+
description: "Show the user a real, rendered UI widget instead of describing one — cards, hero sections, pricing tables, forms, menus, checklists, profiles, galleries. It runs the Plitzi SDK fully offline: no backend, account, or setup. Reach for it whenever a visual layout beats prose: the user asks you to design/build/show something, OR your answer is naturally visual (a recipe → a card, a comparison → a table, steps → a checklist). Prefer showing over telling.\n\nAuthor the widget as an ordered list of `operations` that build an element tree under the pre-seeded root page \"render\". Three rules:\n1. STRUCTURE — one upsertElement builds the whole tree: set pageRef:\"render\" and give element a nested `children` array. Each element is { ref (unique), type, subType?, props?, style?, children? }; children render in order. (To attach to something you already made, use a top-level parentRef:\"<existing ref>\" instead.)\n2. STYLE — declare reusable classes with upsertDefinition { ref, desktop:{ …CSS props in kebab-case… } }, then attach via the element style:{ base:[\"<class ref>\"] }. Lay containers out with flex/grid.\n2b. LAYOUT — it renders in a side panel, so width is free and HEIGHT is scarce. Plain containers stack children vertically, which is the tall half-empty default to avoid: put peers (metrics, plans, options, image + text) in a wrapping row — display:flex, flex-direction:row, flex-wrap:wrap, children flex-grow:\"1\" + flex-basis:\"0%\" + min-width — or a grid with grid-template-columns:\"repeat(auto-fit, minmax(160px, 1fr))\". Keep padding 12-16px and gap 8-12px, and let the outer container fill the panel. Stack only what reads in order (heading over paragraph, forms, steps, prose). Watch the SDK defaults: every container has min-width/min-height 50px (set them to \"0\" for rails, dividers, dots and any flex child that must shrink), and heading/paragraph keep the browser's own margins (zero them and space with the parent's gap).\n2c. THEME — it is embedded in the host UI, which MAY BE DARK, so never hardcode a light palette. Take colours from the host variables with a light-dark() fallback — background-color:\"var(--color-background-secondary, light-dark(#ffffff, #1f2430))\", color:\"var(--color-text-primary, light-dark(#0f172a, #e8eaed))\", border-color:\"var(--color-border-primary, light-dark(#e2e8f0, #333a48))\" — and always set `color` wherever you set `background-color` (a brand accent states its own text colour too).\n3. CONTENT — visible copy goes in props.content (text, heading, paragraph, button); heading level is the element subType (\"h1\"..\"h6\"); image/video take props.src. An unknown prop comes back as a warning naming the right one.\n\nCommon types: container, heading, paragraph, text, button, link, image, video, list, listItem, markdown (plitzi://render/types lists every built-in type with descriptions). Widgets can also be data-driven and interactive — an apiContainer fetches at runtime, upsertBinding wires data into elements, and upsertInteractionFlow makes them react to clicks (see the guide).\nREAD the resource plitzi://render/guide first — it has the element/prop table, the style model and a full worked example, and following it is the difference between a widget that renders and repeated failed calls.\nReturns a compact summary (the widget is shown to the user); on failure it returns teachable errors (path + hint) — read them and retry.",
|
|
109
109
|
inputShape: renderShape,
|
|
110
110
|
access: "read",
|
|
111
111
|
spaceless: true,
|
|
@@ -25,10 +25,17 @@ var resourceMetadataUrl = (config, req) => `${issuerOf(config, req)}${PROTECTED_
|
|
|
25
25
|
* describes `https://host/mcp` while the bare path describes the origin.
|
|
26
26
|
*
|
|
27
27
|
* A server mounted at the ORIGIN is `https://host/`, WITH the trailing slash: a host parses the URL the user
|
|
28
|
-
* typed before it does anything with it, and an empty path renders as `/` —
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* typed before it does anything with it, and an empty path renders as `/` — Claude's connector was observed
|
|
29
|
+
* asking for exactly `resource=https%3A%2F%2Fhost%2F`, so that is the form the document must claim.
|
|
30
|
+
*
|
|
31
|
+
* It does NOT make a bare-origin connector work, and nothing here can. Claude's remote-connector service will
|
|
32
|
+
* not open a session against an MCP endpoint whose URL has no path: the whole grant succeeds — discovery,
|
|
33
|
+
* registration, consent, a 200 from /token with the scope it asked for — and then it reports
|
|
34
|
+
* `McpAuthorizationError` ("the integration rejected the credentials it just issued") without ever presenting
|
|
35
|
+
* the bearer. Verified against the same deployment, minutes apart: at /mcp the token is followed by
|
|
36
|
+
* `initialize`, at / nothing follows it, in the ingress log as well as this server's. Claiming `https://host/mcp`
|
|
37
|
+
* from the bare document would only fail sooner — a client checks that the resource it is handed is no MORE
|
|
38
|
+
* specific than the URL it holds. Publish the connector URL WITH its path. */
|
|
32
39
|
var protectedResourceMetadata = (config, req) => {
|
|
33
40
|
const issuer = issuerOf(config, req);
|
|
34
41
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ServerLogEvent, ServerLogger } from '@plitzi/sdk-shared';
|
|
2
2
|
/** One line for any {@link ServerLogEvent}: an HTTP request reads as an access-log line
|
|
3
3
|
* (`[SSR] 203.0.113.7 GET /pricing 200 12ms ok`), the MCP events as what happened inside one
|
|
4
|
-
* (`[
|
|
4
|
+
* (`[MCP] tools/call plitzi_apply {operations:[3]} 41ms ok`). Rendering is a pure format — the dispatcher
|
|
5
5
|
* already stripped query values, collected no headers, cookies or tokens and summarised tool args by shape;
|
|
6
6
|
* the client IP it does carry is personal data, so a sink that persists these lines must say so. */
|
|
7
7
|
export declare const renderLogEvent: (event: ServerLogEvent) => string;
|
|
@@ -102,5 +102,5 @@ export declare const pageRefOfElement: (schema: Schema, el: Element) => string;
|
|
|
102
102
|
/** Total number of descendant elements under a subtree (excluding the root). */
|
|
103
103
|
export declare const descendantCount: (schema: Schema, rootId: string) => number;
|
|
104
104
|
export declare const emptySpaceMessage = "Space data not available";
|
|
105
|
-
export declare const unauthorizedSpaceMessage
|
|
105
|
+
export declare const unauthorizedSpaceMessage: string;
|
|
106
106
|
export declare const generateObjectId: () => string;
|
|
@@ -22,10 +22,17 @@ export declare const resourceMetadataUrl: (config: OAuthConfig, req: SSRRequest)
|
|
|
22
22
|
* describes `https://host/mcp` while the bare path describes the origin.
|
|
23
23
|
*
|
|
24
24
|
* A server mounted at the ORIGIN is `https://host/`, WITH the trailing slash: a host parses the URL the user
|
|
25
|
-
* typed before it does anything with it, and an empty path renders as `/` —
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* typed before it does anything with it, and an empty path renders as `/` — Claude's connector was observed
|
|
26
|
+
* asking for exactly `resource=https%3A%2F%2Fhost%2F`, so that is the form the document must claim.
|
|
27
|
+
*
|
|
28
|
+
* It does NOT make a bare-origin connector work, and nothing here can. Claude's remote-connector service will
|
|
29
|
+
* not open a session against an MCP endpoint whose URL has no path: the whole grant succeeds — discovery,
|
|
30
|
+
* registration, consent, a 200 from /token with the scope it asked for — and then it reports
|
|
31
|
+
* `McpAuthorizationError` ("the integration rejected the credentials it just issued") without ever presenting
|
|
32
|
+
* the bearer. Verified against the same deployment, minutes apart: at /mcp the token is followed by
|
|
33
|
+
* `initialize`, at / nothing follows it, in the ingress log as well as this server's. Claiming `https://host/mcp`
|
|
34
|
+
* from the bare document would only fail sooner — a client checks that the resource it is handed is no MORE
|
|
35
|
+
* specific than the URL it holds. Publish the connector URL WITH its path. */
|
|
29
36
|
export declare const protectedResourceMetadata: (config: OAuthConfig, req: SSRRequest) => Record<string, unknown>;
|
|
30
37
|
/** RFC 8414. Public clients with PKCE only: a desktop host stores no secret, so `none` is the sole endpoint auth
|
|
31
38
|
* method and S256 the sole challenge method. `offline_access` is advertised whenever refresh grants are issued,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plitzi/sdk-server",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.17",
|
|
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.30.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.17",
|
|
33
|
+
"@plitzi/sdk-schema": "0.32.17",
|
|
34
|
+
"@plitzi/sdk-shared": "0.32.17",
|
|
35
35
|
"ejs": "^6.0.1",
|
|
36
36
|
"esbuild": "^0.28.1",
|
|
37
37
|
"zod": "^4.4.3"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@types/ejs": "^3.1.5",
|
|
45
45
|
"@types/jsdom": "^28.0.3",
|
|
46
46
|
"@types/node": "^26.1.2",
|
|
47
|
-
"@types/react": "^19.2.
|
|
47
|
+
"@types/react": "^19.2.18",
|
|
48
48
|
"@vitejs/plugin-react": "^6.0.5",
|
|
49
49
|
"@vitest/coverage-v8": "^4.1.10",
|
|
50
50
|
"eslint": "^9.39.5",
|