@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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # @plitzi/sdk-server
2
2
 
3
+ ## 0.32.17
4
+
5
+ ### Patch Changes
6
+
7
+ - v0.32.17
8
+ - Updated dependencies
9
+ - @plitzi/plitzi-sdk@0.32.17
10
+ - @plitzi/sdk-schema@0.32.17
11
+ - @plitzi/sdk-shared@0.32.17
12
+
3
13
  ## 0.32.16
4
14
 
5
15
  ### Patch Changes
@@ -8,12 +8,12 @@ var renderRequest = (event) => {
8
8
  };
9
9
  var renderTool = (event) => {
10
10
  const args = event.argsSummary ? ` ${event.argsSummary}` : "";
11
- return `[mcp] tools/call ${event.name}${args} ${Math.round(event.durationMs)}ms ${outcomeOf(event)}`;
11
+ return `[MCP] tools/call ${event.name}${args} ${Math.round(event.durationMs)}ms ${outcomeOf(event)}`;
12
12
  };
13
- var renderResource = (event) => `[mcp] resources/read ${event.name} ${Math.round(event.durationMs)}ms ${outcomeOf(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
- * (`[mcp] tools/call plitzi_apply {operations:[3]} 41ms ok`). Rendering is a pure format — the dispatcher
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 system-ui, sans-serif;
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 tool/resource needs a space, but no spaceId could be resolved from the Authorization token";
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: \`{ "display": "flex", "flex-direction": "column", "gap": "12px" }\`.
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 — a pricing card
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": "card", "desktop": { "display": "flex", "flex-direction": "column", "gap": "8px", "padding": "24px", "background-color": "#ffffff", "border-radius": "12px", "width": "260px", "text-align": "center", "box-shadow": "0 4px 20px rgba(0,0,0,0.08)" } },
112
- { "type": "upsertDefinition", "ref": "price", "desktop": { "font-size": "36px", "font-weight": "800", "color": "#3b82f6" } },
113
- { "type": "upsertDefinition", "ref": "cta", "desktop": { "background-color": "#3b82f6", "color": "#ffffff", "padding": "12px 20px", "border-radius": "8px", "font-weight": "600" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } },
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": "card", "type": "container", "style": { "base": ["card"] },
189
+ "ref": "plans", "type": "container", "style": { "base": ["plans"] },
116
190
  "children": [
117
- { "ref": "plan", "type": "heading", "subType": "h3", "props": { "content": "Pro" } },
118
- { "ref": "amount", "type": "text", "props": { "content": "$29/mo" }, "style": { "base": ["price"] } },
119
- { "ref": "feat", "type": "paragraph", "props": { "content": "Unlimited projects" } },
120
- { "ref": "buy", "type": "button", "props": { "content": "Start free trial" }, "style": { "base": ["cta"] } }
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.16"
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 `/` — that normalized form is what the
29
- * client sends back as the `resource` parameter (asserted in the connector e2e). A document claiming to be
30
- * `https://host` then disagrees with the request made against it, and Claude's connector drops the grant it
31
- * just completed it takes the token and never opens the session. */
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
- * (`[mcp] tools/call plitzi_apply {operations:[3]} 41ms ok`). Rendering is a pure format — the dispatcher
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 = "This tool/resource needs a space, but no spaceId could be resolved from the Authorization token";
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 `/` — that normalized form is what the
26
- * client sends back as the `resource` parameter (asserted in the connector e2e). A document claiming to be
27
- * `https://host` then disagrees with the request made against it, and Claude's connector drops the grant it
28
- * just completed it takes the token and never opens the session. */
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.16",
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.16",
33
- "@plitzi/sdk-schema": "0.32.16",
34
- "@plitzi/sdk-shared": "0.32.16",
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.17",
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",