@pikku/skills 0.12.28 → 0.12.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.28",
3
+ "version": "0.12.30",
4
4
  "description": "The Pikku agent skills — the instruction set coding agents read to build, wire and deploy Pikku projects",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
@@ -2,8 +2,8 @@
2
2
  name: pikku-addon
3
3
  description: >-
4
4
  Use when creating or consuming reusable function packages (addons) in Pikku. Covers wireAddon,
5
- ref(), pikkuAddonServices, pikkuAddonWireServices, addon package structure, and cross-project
6
- function sharing. TRIGGER when: code uses wireAddon/ref()/pikkuAddonServices, user asks about
5
+ ref(), pikkuAddonServices, pikkuAddonWireServices, addon package structure, addons that ship
6
+ database tables (pikku db export), and cross-project function sharing. TRIGGER when: code uses wireAddon/ref()/pikkuAddonServices, user asks about
7
7
  addons, reusable function packages, cross-project sharing, or addon package structure. DO NOT
8
8
  TRIGGER when: user asks about internal function composition (use pikku-wiring) or general function
9
9
  definitions (use pikku-concepts).
@@ -264,6 +264,49 @@ addon` above is the exception — it runs before the addon, and its CLI, exist.
264
264
  addon installs fine and fails to typecheck in every app that depends on it —
265
265
  which is what `pikku validate` is there to catch before you publish.
266
266
 
267
+ ### Database tables
268
+
269
+ An addon may **ship** tables. It must never **create** them: it runs inside the
270
+ consumer, against the consumer's database, so a boot-time `CREATE TABLE` puts a
271
+ second authority on a schema the consumer's migrations own. It declares instead,
272
+ and the consumer's migration history absorbs the declaration.
273
+
274
+ Author the DDL per dialect:
275
+
276
+ ```text
277
+ db/sqlite/0001-labels.sql
278
+ db/postgres/0001-labels.sql
279
+ ```
280
+
281
+ `pikku all` publishes `<outDir>/db/pikku-db-meta.gen.json` on every build — per
282
+ dialect, the SQL verbatim plus a table/column map — and writes it **empty** when
283
+ the addon has no tables, because a consumer reads an absent file as a package
284
+ that cannot say. (`pikku db export` writes the same file on demand.) The
285
+ consumer resolves it **through the package name**, so it must be exported and
286
+ packed, or it never arrives:
287
+
288
+ ```json
289
+ {
290
+ "exports": {
291
+ "./.pikku/db/pikku-db-meta.gen.json": "./dist/.pikku/addon/db/pikku-db-meta.gen.json"
292
+ },
293
+ "files": ["dist"]
294
+ }
295
+ ```
296
+
297
+ **An unresolvable artifact stops `db generate`.** Because the file is
298
+ unconditional, absence means the package cannot say whether it ships tables —
299
+ either it was built with an older CLI, or `exports`/`files` do not carry it. The
300
+ error names both causes. An addon with genuinely no tables is *not* this case:
301
+ it publishes `{}` and is waved through.
302
+
303
+ Two more loud ones: a malformed artifact (missing the SQL for a dialect it
304
+ claims) is rejected rather than half-applied, and an addon publishing only a
305
+ dialect the consumer does not use gets an error naming what it does support.
306
+
307
+ `verifiers/db-schema` runs this end to end on both dialects — copy its addon
308
+ `package.json` when wiring a new one.
309
+
267
310
  ## Consuming an Addon
268
311
 
269
312
  ### Install & Register
@@ -281,6 +324,14 @@ wireAddon({ name: 'todos', package: '@my-org/addon-todos' })
281
324
 
282
325
  After registration, run `yarn pikku all` to generate types for the addon's functions.
283
326
 
327
+ If the addon ships tables, `pikku db generate` then writes one migration per
328
+ addon — named after the package, carrying the addon's own SQL — after Better
329
+ Auth's and the runtime's, so an addon table may reference `user` or a runtime
330
+ table. `pikku db migrate` applies it; re-running `generate` writes nothing once
331
+ covered, and writes only the delta after an addon upgrade. An addon wired with
332
+ `wireRemoteAddon` contributes no schema at all: its tables belong to the
333
+ deployment that runs its functions.
334
+
284
335
  ### Call via RPC
285
336
 
286
337
  ```typescript
@@ -57,7 +57,8 @@ package that declared them, and the extra segment is what stops a linked addon's
57
57
  "./.pikku/pikku-metadata.gen.json": "./dist/.pikku/addon/pikku-metadata.gen.json",
58
58
  "./.pikku/rpc/pikku-rpc-wirings-map.internal.gen.js": {
59
59
  "types": "./dist/.pikku/addon/rpc/pikku-rpc-wirings-map.internal.gen.d.ts"
60
- }
60
+ },
61
+ "./.pikku/db/pikku-db-meta.gen.json": "./dist/.pikku/addon/db/pikku-db-meta.gen.json"
61
62
  },
62
63
  "files": ["dist"],
63
64
  "peerDependencies": {
@@ -23,7 +23,7 @@ than what they look like.
23
23
 
24
24
  | You are… | Read |
25
25
  | --- | --- |
26
- | Defining or invoking an agent — tools, memory, streaming, approval, threads | `references/agents.md` |
26
+ | Defining or invoking an agent — tools, memory, streaming, approval, threads, images | `references/agents.md` |
27
27
  | Wiring the runner, or pointing model strings at a provider or gateway | `references/runner-vercel.md` |
28
28
  | Adding speech in or out of an agent | `references/voice.md` |
29
29
 
@@ -44,7 +44,9 @@ session, the credentials and the RPC depth for you.
44
44
  key. `role`, `personality` and `goal` are concatenated in that order and
45
45
  nothing validates which text lands where, so the split buys legibility only.
46
46
  - **`output` is honoured only when the agent has no tools.** A structured-output
47
- schema on a tool-calling agent is silently inert.
47
+ schema on a tool-calling agent is silently inert. The pairing that wants it is
48
+ reading an image or a document into typed data — one tool-free agent, an
49
+ `output` schema, and the picture passed as an `attachments` entry.
48
50
  - **`auth` defaults to `false`**, because agents are normally invoked from an
49
51
  already-authenticated `pikkuFunc`. `scopes` and `permissions` are enforced
50
52
  either way — see `pikku-auth`.
@@ -201,6 +201,36 @@ export const structuredAgent = pikkuAgent({
201
201
  })
202
202
  ```
203
203
 
204
+ ### Images and files
205
+
206
+ `attachments` on the agent input is how a picture, a scan or a PDF reaches the
207
+ model. Each entry carries **either** `data` (base64, no `data:` prefix) **or**
208
+ `url`, plus a `mediaType`:
209
+
210
+ ```typescript
211
+ const { object } = await rpc.agent.run('read-receipt', {
212
+ message: 'List every line item on this receipt.',
213
+ threadId, resourceId,
214
+ attachments: [{ type: 'image', data: base64Jpeg, mediaType: 'image/jpeg' }],
215
+ })
216
+ ```
217
+
218
+ - **`url` is downloaded server-side**, by the runner, before the model sees it —
219
+ so a caller-supplied URL is an SSRF surface. `VercelAgentRunner`'s third
220
+ constructor argument is a host allowlist; set it whenever an HTTP wiring
221
+ accepts attachment URLs, or take `data` and never accept a URL at all.
222
+ - **The model has to be a vision model.** The provider prefix does not decide
223
+ this — `openai/gpt-5-mini` reads images, a text-only id in the same family
224
+ answers as though the attachment were not there rather than erroring.
225
+ - **Reading an image into data is the tool-free case**, so it can use `output`:
226
+ an agent with an `output` schema and no tools resolves `result.object` as the
227
+ typed extraction. Add one tool and you get prose back instead — see
228
+ **Structured output** above.
229
+ - **Base64 is the request body.** A phone photo is measured in megabytes and
230
+ goes through your function's input schema, the RPC payload and the model's
231
+ context. Downscale on the client before uploading — the model does not read
232
+ the pixels you paid to send.
233
+
204
234
  ### Narrowing tools per step
205
235
 
206
236
  `prepareStep` runs before each step with the live tool array for that step, so
@@ -33,9 +33,11 @@ plus more effort" — it is App plus a deliberate surface checklist, so read the
33
33
  base first and follow it in full rather than blending the two into one plan.
34
34
 
35
35
  The supporting references belong to whichever mode sends you to them:
36
- `references/multi-app.md` (a second frontend), `references/theming.md`
37
- (authoring the theme), `references/ship.md` (deploying, and the Fabric-readiness
38
- contract).
36
+ `references/multi-app.md` (a second frontend), `references/design.md` (committing
37
+ to a design direction and judging whether the screens realise it — read before
38
+ the first screen is built, not after the last), `references/theming.md`
39
+ (authoring the theme),
40
+ `references/ship.md` (deploying, and the Fabric-readiness contract).
39
41
 
40
42
  ## Bootstrap before anything else
41
43
 
@@ -418,6 +418,12 @@ over, and an uncovered function is a half-milestone whether or not the note says
418
418
  file. Compose the kit from `@/components/<Name>` rather than hand-rolling.
419
419
  Register the screen in `useNavItems()` — that one file feeds both the desktop
420
420
  sidebar and the phone navigation.
421
+ **Read `references/design.md` before you write the first screen.** You commit
422
+ to a design direction there and are then accountable to it — it hands you no
423
+ layouts, because the design is yours to make. Screenshot each screen at 390
424
+ and 1440 with the seed in place at the END of every milestone, and look at the
425
+ images — not once at §8, where the only affordable fix is a repaint of eight
426
+ screens.
421
427
  6. **Scenario** (§7), then `status: built`.
422
428
 
423
429
  Rules that are not optional:
@@ -635,17 +641,29 @@ loud is a number nobody acts on.
635
641
 
636
642
  ## 8. Make it look like someone designed it
637
643
 
638
- Two separate jobs, and conflating them is why open-source builds come out looking
639
- like the template:
640
-
641
- - **8a. Direction** — deciding what it should look like. **No open-source tool
642
- does this.** Fabric has `fabric-theme`; you have §1's answer and this section.
643
- - **8b. Critique** — judging how well the built screens execute that direction.
644
+ **This section numbers 8, but half of it has already happened.** Read
645
+ `references/design.md` before the first screen is built — a design pass run on
646
+ eight milestones of scaffolded screens is a repaint, and it shows. What is left
647
+ here at §8 is the theme you may have deferred and the critique you cannot run
648
+ until there are screens to critique.
649
+
650
+ Three separate jobs, and conflating them is why open-source builds come out
651
+ looking like the template:
652
+
653
+ - **Direction** — deciding what it should look like. **No open-source tool does
654
+ this.** Fabric has `fabric-theme`; you have §1's answer and 8a below.
655
+ - **Execution** — whether the screens actually realise that direction, or
656
+ default to whatever component was nearest. No theme does this, and it is where
657
+ "works but looks like nobody decided anything" comes from.
658
+ `references/design.md` carries the process for it, and it belongs at §6, per
659
+ screen.
660
+ - **Critique** — judging how well the built screens execute the direction.
644
661
  `impeccable` does this well, and it is free.
645
662
 
646
663
  Impeccable audits the design you chose. It will never tell you the app should
647
664
  have looked like something else — it will happily award a clean bill of health to
648
- a perfectly-executed default. Skip 8a and you ship Neutral with good spacing.
665
+ a perfectly-executed default. Skip the first two and you ship Neutral with good
666
+ spacing.
649
667
 
650
668
  ### 8a. Author the theme — the step nothing does for you
651
669
 
@@ -674,6 +692,13 @@ theme is; only the note records why.
674
692
 
675
693
  ### 8b. Compose real components, then critique
676
694
 
695
+ **`references/design.md` is where design actually lives** — committing to a
696
+ direction before the first screen, judging the result from screenshots rather
697
+ than from source, the two screens that get skipped, and why you reset the dev
698
+ database before judging anything. It prescribes no layouts on purpose: two apps
699
+ built from this skill should not look like each other. What follows here is only
700
+ the component inventory.
701
+
677
702
  **Compose with Mantine's rich components — not tables and text everywhere:**
678
703
 
679
704
  - **`@mantine/charts`** (Recharts underneath) for overviews — `AreaChart`,
@@ -710,8 +735,17 @@ a modal taller than the viewport. Mantine gives you the tools (responsive `Grid`
710
735
  them. The template already mounts a phone navigation per `AGENTS.md` — pick
711
736
  `MobileTabBar` or `MobileNavDrawer` deliberately per app, never both.
712
737
 
713
- The gate: **no P0 findings left on any screen, in any app, at either width.**
714
- Don't silence a finding by deleting the feature it is about.
738
+ The gate: **no P0 findings left on any screen, in any app, at either width**,
739
+ and every screen honestly answers the questions in `references/design.md` —
740
+ including whether it looks like the direction you committed to or like the
741
+ components you had. Don't silence a finding by deleting the feature it is
742
+ about.
743
+
744
+ **Critique the data too, not just the layout.** Scenario runs write run-tagged
745
+ rows into the dev database, so by §8 the app is full of `Ripe peaches d193e2aa`
746
+ seven copies deep. Run `pikku db reset` and restart the dev server before you
747
+ screenshot anything — a screen judged against that data gets designed for a
748
+ problem it does not have.
715
749
 
716
750
  ## 9. Ship it, and stay Fabric-ready
717
751
 
@@ -735,6 +769,8 @@ cheaper to honour than to retrofit:
735
769
 
736
770
  - `references/multi-app.md` — adding a second frontend (§4), at the milestone
737
771
  that needs it
772
+ - `references/design.md` — committing to a design direction, and how to tell
773
+ whether the screens realise it. Read BEFORE the first screen (§6), not at §8
738
774
  - `references/theming.md` — authoring the theme (§8a)
739
775
  - `references/ship.md` — deploying, and the Fabric-readiness contract (§9)
740
776
  - Sibling skills: `pikku-knowledge` (§2), `pikku-auth` (§3),
@@ -0,0 +1,128 @@
1
+ # Design
2
+
3
+ Your job here is to design something worth the product — not to apply a house
4
+ style. **There is no house style, and this file is not one.** Two apps built from
5
+ this skill should not look like each other; if they do, something has gone wrong
6
+ that no amount of spacing will fix.
7
+
8
+ So: no prescribed layouts, no component rules, no ratios. What follows is the
9
+ process that makes freedom accountable, the handful of facts that are not
10
+ matters of taste, and the symptoms of a screen nobody actually designed.
11
+
12
+ ## Commit to a direction before the first screen
13
+
14
+ An agent given "make it look good" and nothing else defaults — to the component
15
+ nearest to hand, on every screen, in every app. Not because it lacks taste, but
16
+ because there is nothing to be wrong against. A direction fixes that: state one,
17
+ in words, before any screen exists.
18
+
19
+ Say what this product *is* — its register, its reference points, what it feels
20
+ like to use, what it is deliberately not. "A warm, food-forward thing you use
21
+ standing at an open fridge door, closer to a recipe card than to a dashboard, and
22
+ never clinical" is a direction. "Clean and modern" is not — it rules nothing out,
23
+ so it cannot be departed from.
24
+
25
+ Write it to `knowledge/decisions/design/`, then build the theme from it
26
+ (`references/theming.md`). **From that point you are accountable to your own
27
+ direction, not to this file.** That is the whole mechanism: the freedom is real,
28
+ and so is the commitment.
29
+
30
+ Be ambitious with it. A direction that could describe any SaaS app has not been
31
+ chosen — it has been defaulted to in words instead of in components.
32
+
33
+ ## One screen, designed properly, before the rest exist
34
+
35
+ Design the first real screen as if it were the only one, and take it further than
36
+ feels necessary. Every screen after it inherits its register — its density, its
37
+ rhythm, what a row of data looks like, how state is signalled. That inheritance
38
+ happens whether you plan it or not, so make the first one worth inheriting.
39
+
40
+ This is also the cheapest design work in the project. The eighth screen is a
41
+ repaint of eight; the first is a decision.
42
+
43
+ ## Judge it — and don't grade your own homework
44
+
45
+ Models rate their own output generously, and "does this look good?" answered by
46
+ the thing that made it is always yes. Use evidence.
47
+
48
+ - **Screenshot every screen and look at the image**, at ~390px and at ~1440px.
49
+ Judging your own UI from source is guessing, and the failures that matter —
50
+ proportion, hierarchy, a wall of identical boxes — are invisible in JSX.
51
+ - **Run `impeccable`** (`npx impeccable install`, Node 22.18+) and feed it the
52
+ screenshots. It is external, it does not flatter, and it scores execution
53
+ against interaction heuristics. But it audits how well you executed the design
54
+ you chose — it will award a clean bill of health to a perfectly executed
55
+ default. It checks step 2; it never replaces it.
56
+ - **Look at every milestone, not once at the end.** A screen that was fine at
57
+ three rows is a different screen at sixty, and the milestone that added the
58
+ sixty is the cheapest place to notice.
59
+
60
+ Questions worth answering honestly, per screen. The answers are yours; only the
61
+ questions are fixed:
62
+
63
+ 1. What question does someone open this screen to ask, and how fast do they get
64
+ the answer?
65
+ 2. What can be understood before reading a word?
66
+ 3. What is here that is not earning its space?
67
+ 4. What does it look like at real volume, and at 390px?
68
+ 5. Does it look like the direction you committed to — or like the components you
69
+ had?
70
+ 6. Would you show it to the user without apologising for it?
71
+
72
+ ## Facts, not taste
73
+
74
+ These are not design opinions and are not open to a different answer.
75
+
76
+ - **Reset the dev database before judging anything.** Scenario runs deliberately
77
+ tag rows with a run id so assertions do not collide, so after a few runs the
78
+ app is full of `Ripe peaches d193e2aa`, seven copies deep. Nobody's data looks
79
+ like that, and a screen designed against it is designed for a problem it does
80
+ not have. `pikku db reset` replays the migrations and the seed — and the dev
81
+ server holds an open handle to the file, so **restart it after**, or every call
82
+ fails with `disk I/O error` and the app looks broken for reasons that are not
83
+ the app.
84
+ - **Seed generously and realistically.** A seed with a deliberate spread designs
85
+ the hard cases for you. Three identical rows teach you nothing.
86
+ - **Never render a sign-in method that is not configured.** A "Continue with
87
+ Google" button on an app with no Google credentials is a dead control on the
88
+ first screen anyone sees. Render social buttons from what `socialProviders`
89
+ actually declares — and when it declares none, the divider goes too.
90
+ - **Empty, loading and error are states that exist.** The empty state is what a
91
+ new user meets first and the one most often skipped entirely.
92
+ - **Contrast and tap targets are measured, not judged.** `pikku-a11y` covers it.
93
+ One trap worth knowing: a Mantine `light` variant paints its label at the
94
+ generated ramp's stop, which lands just under AA on its own tint — name the
95
+ darker ink once and reuse it.
96
+
97
+ ## Two screens that get skipped
98
+
99
+ Not rules about how they should look — only that they are yours to design.
100
+
101
+ **The login page is the entry point.** It is the first thing anyone sees, where
102
+ every demo starts, and routinely the least designed screen in the app: a default
103
+ card with a wordmark, saying "scaffold" before the product has said anything. It
104
+ gets the same direction as everything else.
105
+
106
+ **The navigation is on every screen**, which makes it the highest-leverage
107
+ surface you have. The scaffold mounts one that works; working is not the same as
108
+ considered. Decide the destinations and their number deliberately, and style it
109
+ through the theme rather than leaving the component defaults — the same look on
110
+ every app is the tell.
111
+
112
+ ## Symptoms of a screen nobody designed
113
+
114
+ If a screen shows these, you defaulted. **The fix is yours to choose** — these
115
+ are a diagnosis, not a prescription, and the interesting answer is rarely the
116
+ first one.
117
+
118
+ - The top of the screen is a form, and the content it acts on starts below the
119
+ fold.
120
+ - Two adjacent surfaces do the same job because both were added separately.
121
+ - Everything is the same size, weight and colour, so nothing can be found
122
+ without reading it.
123
+ - The same fact is stated twice in one line, in two different components.
124
+ - A section named for an exception holds half the data.
125
+ - Every row carries the same buttons, and the buttons outweigh the content.
126
+ - The palette's meaningful colours are also used decoratively, so they have
127
+ stopped meaning anything.
128
+ - It looks like the last app you built.
@@ -285,6 +285,39 @@ script, run `pikku dev` from the **project root** (it resolves `srcDirectories`
285
285
  relative to the config, so a nested cwd yields a doubled watch path and no hot
286
286
  reload).
287
287
 
288
+ ## Reaching a model
289
+
290
+ An agent needs an `agentRunner` in singleton services, and locally you do not
291
+ write one: `pikku dev` builds it from env when it finds a **matching pair** —
292
+ `OPENAI_BASE_URL` + `OPENAI_API_KEY`, or `LITELLM_PROXY_URL` + `LITELLM_API_KEY`
293
+ — and registers it under `'*'`, so every `provider/model` prefix resolves
294
+ through it. With neither pair complete it builds nothing and every agent call
295
+ fails with `AIProviderNotConfiguredError` (a 503) — which reads like a broken
296
+ agent rather than a missing key, so check `.env` first. Mixing halves is worse
297
+ than missing them: a URL from one source with a key from the other 401s on every
298
+ call, so the pairs are taken whole, OpenAI first.
299
+
300
+ Two ways to fill them in:
301
+
302
+ **The Fabric AI gateway.** One key, and every model the gateway fronts —
303
+ OpenAI, Anthropic, Google, the OpenRouter catalogue — is reachable by id, billed
304
+ through your Fabric account rather than per-vendor:
305
+
306
+ ```bash
307
+ pikku fabric login
308
+ pikku fabric llm key --env >> .env # writes both pairs; --shell and --json also exist
309
+ ```
310
+
311
+ It mints or reuses a developer-scoped key against your Fabric login. `link` is
312
+ not required — the key is yours, not the project's.
313
+
314
+ **Your own vendor key.** `OPENAI_BASE_URL=https://api.openai.com/v1` with your
315
+ `OPENAI_API_KEY`, and model ids are then only the ones that vendor serves.
316
+
317
+ A deployed stage takes the same names through `pikku fabric secrets set` /
318
+ `variables set` — the agent units get their runner wired by the bundler, from
319
+ those values.
320
+
288
321
  ## Deploy
289
322
 
290
323
  ```bash
@@ -30,6 +30,16 @@ request-scoped logger or audit buffer is a wire service, and startup work that
30
30
  needs the singletons goes in `pikkuServerLifecycle` rather than in a module's
31
31
  top level.
32
32
 
33
+ ## Services the runtime injects
34
+
35
+ `pikku dev` and `pikku serve` build a set of singletons before your
36
+ `createSingletonServices` runs and hand them in as `existingServices` — among
37
+ them `content`, a `LocalContent` storing files under `.pikku-runtime/content`
38
+ and serving them from `/upload` and `/assets`. **You never construct these in
39
+ `services.ts`, and their absence from that file is not evidence they are off.**
40
+ The optional `content` block in `pikku.config.json` only overrides that
41
+ service's paths and size limit; omitting it does not disable it.
42
+
33
43
  ## Pick the reference
34
44
 
35
45
  | You are… | Read |
@@ -166,49 +166,25 @@ export const deleteTodo = pikkuFunc({
166
166
  })
167
167
  ```
168
168
 
169
- ## MCP Server Setup
169
+ ## Reaching the server
170
170
 
171
- `PikkuMCPServer` takes the server config and a logger — not your services. It
172
- loads the generated `mcp.gen.json`, and the bootstrap import is what registers
173
- your functions.
171
+ You do not start an MCP server. `pikku dev`, `pikku serve` and a deployed app all
172
+ mount one for you: codegen writes `.pikku/mcp/mcp.gen.json`, the runtime loads it,
173
+ and the server is served at **`/mcp`** — so a tool you export is reachable at
174
+ `https://<your-app-url>/mcp` with nothing else to wire. Set `mcpPath` to move it.
174
175
 
175
- ```typescript
176
- // start.ts
177
- import { PikkuMCPServer } from '@pikku/modelcontextprotocol'
178
- import { createConfig, createSingletonServices } from './services.js'
179
- import mcpJSON from '../.pikku/mcp/mcp.gen.json' with { type: 'json' }
180
- import '../.pikku/pikku-bootstrap.gen.js'
181
-
182
- const config = await createConfig()
183
- const singletonServices = await createSingletonServices(config)
184
-
185
- const server = new PikkuMCPServer(
186
- {
187
- name: 'pikku-mcp-server',
188
- version: '1.0.0',
189
- mcpJSON,
190
- capabilities: { logging: {}, tools: {}, resources: {}, prompts: {} },
191
- },
192
- singletonServices.logger
193
- )
194
-
195
- await server.init()
196
-
197
- // stdio — the transport desktop MCP clients spawn
198
- await server.connectStdio()
199
- singletonServices.logger = server.createMCPLogger()
200
-
201
- // …or streamable HTTP, for a hosted server
202
- const { close } = await server.connectHTTP({ port: 3000, host: '127.0.0.1' })
203
- ```
176
+ Two consequences worth stating outright, because both read as breakage:
204
177
 
205
- `capabilities` is a filter, not documentation: a surface you leave out is not
206
- advertised and its endpoints are never loaded, which is how you ship a tools-only
207
- server.
178
+ - The mount is **conditional on there being something to serve**. An `mcp.gen.json`
179
+ with no tools, resources or prompts is not mounted at all, so `/mcp` 404s until
180
+ the first `mcp: true` function or `pikkuMCP*Func` exists.
181
+ - There is no `wires/mcp` directory and no `mcp` block in `pikku.config.json`.
182
+ A tool *is* its own registration, so the absence of both is what correct MCP
183
+ wiring looks like — not evidence that something was missed.
208
184
 
209
- Over stdio the protocol owns stdout, so an ordinary console logger corrupts the
210
- frames — that is what `createMCPLogger()` is for. Swap the logger before
211
- anything logs.
185
+ When you add a tool, tell whoever asked for it the URL. An assistant that cannot
186
+ be pointed at an endpoint has not been connected to anything, and `/mcp` is the
187
+ whole answer.
212
188
 
213
189
  ## Red flags
214
190
 
@@ -218,4 +194,4 @@ anything logs.
218
194
  | `uri`/`title` rejected on `pikkuMCPResourceFunc` | Those belong on `wireMCPResource` |
219
195
  | Resource returning `{ uri, blob, mimeType }` | Resources are text only: `{ uri, text }` |
220
196
  | Client sees a tool with no description | `mcp: true` without a `description` — check the codegen warning |
221
- | stdio client disconnects on the first log line | Logger still writing to stdout; use `createMCPLogger()` |
197
+ | `/mcp` 404s | Nothing to serve yet — the mount is skipped until one tool, resource or prompt exists |