@pikku/skills 0.12.4 → 0.12.6

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.
Files changed (64) hide show
  1. package/dist/skills.gen.js +1 -1
  2. package/package.json +1 -1
  3. package/skills/pikku-addon/SKILL.md +56 -29
  4. package/skills/pikku-ai-agent/SKILL.md +197 -105
  5. package/skills/pikku-ai-vercel/SKILL.md +57 -18
  6. package/skills/pikku-ai-voice/SKILL.md +126 -52
  7. package/skills/pikku-audit/SKILL.md +35 -13
  8. package/skills/pikku-aws/SKILL.md +66 -16
  9. package/skills/pikku-backblaze/SKILL.md +44 -11
  10. package/skills/pikku-better-auth/SKILL.md +45 -10
  11. package/skills/pikku-cli/SKILL.md +67 -18
  12. package/skills/pikku-cli/references/complete-example.md +2 -0
  13. package/skills/pikku-concepts/SKILL.md +75 -10
  14. package/skills/pikku-config/SKILL.md +56 -14
  15. package/skills/pikku-cron/SKILL.md +13 -6
  16. package/skills/pikku-deploy-azure/SKILL.md +83 -28
  17. package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
  18. package/skills/pikku-deploy-express/SKILL.md +40 -4
  19. package/skills/pikku-deploy-fastify/SKILL.md +22 -1
  20. package/skills/pikku-deploy-lambda/SKILL.md +99 -19
  21. package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
  22. package/skills/pikku-deploy-uws/SKILL.md +54 -1
  23. package/skills/pikku-deps/SKILL.md +29 -8
  24. package/skills/pikku-emails/SKILL.md +36 -5
  25. package/skills/pikku-fabric/SKILL.md +27 -2
  26. package/skills/pikku-fabric-debug/SKILL.md +5 -1
  27. package/skills/pikku-feature/SKILL.md +6 -1
  28. package/skills/pikku-gateway-slack/SKILL.md +72 -11
  29. package/skills/pikku-http/SKILL.md +18 -5
  30. package/skills/pikku-http/references/http-options.md +10 -5
  31. package/skills/pikku-i18n/SKILL.md +18 -7
  32. package/skills/pikku-info/SKILL.md +18 -8
  33. package/skills/pikku-jose/SKILL.md +35 -6
  34. package/skills/pikku-kysely/SKILL.md +78 -15
  35. package/skills/pikku-machine-auth/SKILL.md +36 -1
  36. package/skills/pikku-mcp/SKILL.md +159 -149
  37. package/skills/pikku-middleware/SKILL.md +17 -5
  38. package/skills/pikku-mongodb/SKILL.md +10 -2
  39. package/skills/pikku-n8n-import/SKILL.md +14 -6
  40. package/skills/pikku-permissions/SKILL.md +102 -22
  41. package/skills/pikku-pino/SKILL.md +12 -4
  42. package/skills/pikku-product-second-opinion/SKILL.md +3 -3
  43. package/skills/pikku-queue/SKILL.md +45 -16
  44. package/skills/pikku-react/SKILL.md +41 -14
  45. package/skills/pikku-react-query/SKILL.md +14 -10
  46. package/skills/pikku-realtime/SKILL.md +44 -22
  47. package/skills/pikku-redis/SKILL.md +12 -3
  48. package/skills/pikku-rpc/SKILL.md +23 -12
  49. package/skills/pikku-rtl/SKILL.md +21 -17
  50. package/skills/pikku-scenario/SKILL.md +108 -75
  51. package/skills/pikku-schedule/SKILL.md +39 -6
  52. package/skills/pikku-schema-ajv/SKILL.md +24 -2
  53. package/skills/pikku-schema-cfworker/SKILL.md +22 -2
  54. package/skills/pikku-security/SKILL.md +54 -9
  55. package/skills/pikku-services/SKILL.md +49 -9
  56. package/skills/pikku-services/references/audit-wire-service.md +2 -1
  57. package/skills/pikku-template-clone/SKILL.md +10 -5
  58. package/skills/pikku-trigger/SKILL.md +50 -6
  59. package/skills/pikku-versioning/SKILL.md +46 -17
  60. package/skills/pikku-websocket/SKILL.md +72 -44
  61. package/skills/pikku-workflow/SKILL.md +35 -1
  62. package/skills/pikku-workflow/references/workflow-reference.md +13 -8
  63. package/skills/pikku-workflows-client/SKILL.md +13 -6
  64. package/skills/pikku-ws/SKILL.md +44 -8
@@ -28,7 +28,7 @@ Use this skill as an execution checklist, not reference material.
28
28
 
29
29
  ## Writing Queries — the Kysely query builder
30
30
 
31
- In a Pikku function body the injected `kysely` IS the `Kysely<DB>` instance — query it directly. Pikku wires the **CamelCasePlugin**, so you write **camelCase everywhere in TS** (columns, aliases) and raw **snake_case ONLY inside a `` sql`` `` literal**. Kysely is a query builder, NOT an ORM — there are no relations; shape nested data with the JSON helpers below. Never hand-roll SQL strings; never annotate the return type (in Pikku the output zod schema IS the type).
31
+ In a Pikku function body the injected `kysely` IS the `Kysely<DB>` instance — query it directly. Every connection factory below wires the **CamelCasePlugin** by default, so you write **camelCase everywhere in TS** (columns, aliases) and raw **snake_case ONLY inside a `` sql`` `` literal**. If a project opted out (`createNodeSqliteKysely({ camelCase: false })`), that inverts — check how the instance was built before assuming. Kysely is a query builder, NOT an ORM — there are no relations; shape nested data with the JSON helpers below. Never hand-roll SQL strings; never annotate the return type (in Pikku the output zod schema IS the type).
32
32
 
33
33
  ```typescript
34
34
  import { sql } from 'kysely'
@@ -92,12 +92,19 @@ await kysely.transaction().execute(async (trx) => {
92
92
  })
93
93
  ```
94
94
 
95
- Pikku provides SQL database services through four packages:
95
+ Pikku provides SQL database services through six packages:
96
96
 
97
- - `@pikku/kysely` — Base service implementations (database-agnostic)
98
- - `@pikku/kysely-postgres` — PostgreSQL-specific implementations + `PikkuKysely` connection wrapper
97
+ - `@pikku/kysely` — Base service implementations (database-agnostic), the serialize plugins, `createAuditedKysely` and the `pikkuSchemas` helpers
98
+ - `@pikku/kysely-postgres` — PostgreSQL-specific implementations + the `PikkuKysely` connection wrapper and `PgEventHubService` (LISTEN/NOTIFY-backed)
99
99
  - `@pikku/kysely-mysql` — MySQL-specific implementations
100
- - `@pikku/kysely-sqlite` — SQLite-specific implementations + `createSQLiteKysely` factory
100
+ - `@pikku/kysely-sqlite` — SQLite-specific implementations, `createSQLiteKysely`, and the `LibsqlWebDialect`
101
+ - `@pikku/kysely-node-sqlite` — `createNodeSqliteKysely` over `node:sqlite`, plus user-defined SQL functions and the coercion plugin
102
+ - `@pikku/kysely-bun-sqlite` — the same over `bun:sqlite`
103
+
104
+ The last two are runtime adapters rather than service sets: they build the
105
+ `Kysely<DB>` you inject into functions, while the dialect packages above supply
106
+ Pikku's own stores. They differ in one place — `bun:sqlite` cannot register
107
+ scalar functions, so `createBunSqliteKysely` throws if you pass `functions`.
101
108
 
102
109
  All implement standard Pikku interfaces from `@pikku/core`.
103
110
 
@@ -105,9 +112,11 @@ All implement standard Pikku interfaces from `@pikku/core`.
105
112
 
106
113
  ```bash
107
114
  # Pick your database
108
- yarn add @pikku/kysely @pikku/kysely-postgres # PostgreSQL
109
- yarn add @pikku/kysely @pikku/kysely-mysql # MySQL
110
- yarn add @pikku/kysely @pikku/kysely-sqlite # SQLite
115
+ yarn add @pikku/kysely @pikku/kysely-postgres # PostgreSQL
116
+ yarn add @pikku/kysely @pikku/kysely-mysql # MySQL
117
+ yarn add @pikku/kysely @pikku/kysely-sqlite # SQLite (stores)
118
+ yarn add @pikku/kysely-node-sqlite # SQLite on Node
119
+ yarn add @pikku/kysely-bun-sqlite # SQLite on Bun
111
120
  ```
112
121
 
113
122
  ## API Reference
@@ -120,7 +129,8 @@ import { PikkuKysely } from '@pikku/kysely-postgres'
120
129
  const db = new PikkuKysely<DB>(
121
130
  logger: Logger,
122
131
  connectionOrConfig: postgres.Sql | postgres.Options | string,
123
- defaultSchemaName?: string
132
+ defaultSchemaName?: string,
133
+ poolConfig?: PostgresConfig // maxPool, connectTimeout, idleTimeout, maxLifetime, prepare, statementTimeout
124
134
  )
125
135
 
126
136
  await db.init()
@@ -128,14 +138,39 @@ db.kysely // Kysely<DB> instance for queries
128
138
  await db.close()
129
139
  ```
130
140
 
131
- ### SQLite Factory — `createSQLiteKysely`
141
+ It builds a postgres.js-backed Kysely with the CamelCasePlugin. Pass an existing
142
+ `postgres.Sql` when something else owns the pool — the wrapper then leaves it
143
+ open on `close()`. `poolConfig` keys are only forwarded when set, so postgres.js
144
+ keeps its own defaults for the rest, and it is ignored entirely when you hand in
145
+ an already-constructed connection.
146
+
147
+ ### SQLite factories
148
+
149
+ ```typescript
150
+ import { createNodeSqliteKysely } from '@pikku/kysely-node-sqlite'
151
+
152
+ // Your application DB — CamelCasePlugin on by default
153
+ const kysely = createNodeSqliteKysely<DB>({
154
+ filename: 'app.db', // or ':memory:'
155
+ camelCase: true,
156
+ plugins: [], // layered on top
157
+ functions: {}, // scalar UDFs, registered as deterministic (Node only)
158
+ })
159
+ ```
132
160
 
133
161
  ```typescript
134
162
  import { createSQLiteKysely } from '@pikku/kysely-sqlite'
135
163
 
136
- const kysely = createSQLiteKysely(database: SqliteDatabase | (() => Promise<SqliteDatabase>))
164
+ // Pikku's own tables — returns Kysely<KyselyPikkuDB>, not your DB
165
+ const pikkuDb = createSQLiteKysely(database: SqliteDatabase | (() => Promise<SqliteDatabase>))
137
166
  ```
138
167
 
168
+ These two are not interchangeable. `createSQLiteKysely` is typed to
169
+ `KyselyPikkuDB` and wires the `SerializePlugin` (JSON columns in and out) rather
170
+ than the CamelCasePlugin, because it exists to back the stores below. Reach for
171
+ `createNodeSqliteKysely` / `createBunSqliteKysely` for the instance your
172
+ functions query.
173
+
139
174
  ### Available Services
140
175
 
141
176
  Each database variant exports these services with a prefix (`Pg`, `MySQL`, `SQLite`, or base `Kysely`):
@@ -151,24 +186,52 @@ Each database variant exports these services with a prefix (`Pg`, `MySQL`, `SQLi
151
186
  | `*AgentRunService` | `AgentRunService` | Agent execution tracking |
152
187
  | `*SecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
153
188
 
189
+ A handful more live only on the base package — there is no `Pg`/`MySQL`/`SQLite`
190
+ variant to reach for, you import them from `@pikku/kysely` whatever the engine:
191
+
192
+ | Service | Purpose |
193
+ | -------------------------- | --------------------------------------------- |
194
+ | `KyselySessionStore` | Persisted user sessions |
195
+ | `KyselyScopeService` | Scope and role storage |
196
+ | `KyselyWebhookService` | Webhook registrations and deliveries |
197
+ | `KyselyCredentialService` | Encrypted third-party credentials |
198
+ | `KyselyAIRunStateService` | AI run state (also implemented by AIStorage) |
199
+ | `KyselyWorkflowMirror` | Mirrors workflow runs into queryable tables |
200
+ | `KyselyAuditService` | Durable audit sink (see `pikku-audit`) |
201
+
154
202
  All services take a `Kysely<KyselyPikkuDB>` instance in their constructor and have an `init()` method that creates tables if needed.
155
203
 
156
204
  ### Secret Service
157
205
 
206
+ Envelope encryption: each secret gets its own DEK, wrapped by a KEK derived from
207
+ `key` plus a stored per-version salt. Keeping `previousKey` around is what makes
208
+ rotation possible — `rotateKEK` re-wraps every secret from the old key to the
209
+ current one and returns the new version, and it throws if no `previousKey` is
210
+ configured.
211
+
158
212
  ```typescript
159
213
  import { PgKyselySecretService } from '@pikku/kysely-postgres'
160
214
 
161
215
  const secrets = new PgKyselySecretService(db.kysely, {
162
- kekSecret: 'your-key-encryption-key',
163
- salt: 'your-salt',
216
+ key: 'your-key-encryption-passphrase',
217
+ keyVersion: 2, // defaults to 1
218
+ previousKey: 'the-passphrase-you-are-rotating-away-from',
219
+ audit: true, // log write/delete/rotate through the audit sink
220
+ auditReads: false, // reads too — noisy, off by default
164
221
  })
165
222
  await secrets.init()
166
223
 
167
224
  await secrets.setSecret('api-key', { key: 'sk-...' })
168
- const value = await secrets.getSecret<{ key: string }>('api-key')
169
- await secrets.rotateKEK() // Re-encrypt all secrets with new KEK
225
+ const secret = await secrets.getSecret<{ key: string }>('api-key')
226
+ await secrets.hasSecret('api-key')
227
+ await secrets.deleteSecret('api-key')
228
+ const newVersion = await secrets.rotateKEK()
170
229
  ```
171
230
 
231
+ `getSecret` hands back a `SecretValue<T>`, not the bare value — it serializes as
232
+ `[secret]` until something reveals it, which is what stops a secret drifting into
233
+ a log line or an audit row. See `pikku-config` for the reveal rules.
234
+
172
235
  ## Usage Patterns
173
236
 
174
237
  ### PostgreSQL Setup
@@ -157,7 +157,42 @@ addHTTPMiddleware([
157
157
 
158
158
  When the api-key header is present it is authoritative — the middleware never
159
159
  falls through to `getSession` (a bare mock session would shadow the scoped one).
160
- When it is absent, the human `getSession` path runs as normal.
160
+ When it is absent, the human `getSession` path runs as normal. Either way the
161
+ middleware bails out entirely if a session is already set, and it checks the
162
+ *live* session rather than the wire's construction-time snapshot, so it can't
163
+ clobber one an earlier middleware resolved.
164
+
165
+ ### Restricting a key below its owner
166
+
167
+ Set `scopes` on the session `mapKey` returns and that set is **authoritative** —
168
+ including an empty one. It is never widened back out to everything the owning
169
+ service user holds:
170
+
171
+ ```typescript
172
+ mapKey: async (key) => ({
173
+ userId: 'sandbox-runtime',
174
+ scopes: ['sandbox:read'], // this key can do only this
175
+ })
176
+ ```
177
+
178
+ Leave `scopes` unset for a key that acts with its owner's full rights. This is
179
+ what makes one stable service user safely able to own keys of very different
180
+ power — the restriction lives on the key, not on a proliferation of identities.
181
+
182
+ ### Failure handling is deliberately split
183
+
184
+ A key that fails to verify is logged and treated as an ordinary "not
185
+ authenticated" — an unusable credential is not an outage. A failure *inside*
186
+ `mapKey` (your scope store is down) propagates as a real error instead. That
187
+ asymmetry is on purpose: a scope lookup that silently failed would serve the
188
+ request anonymously, which is exactly the wrong direction to fail in.
189
+
190
+ ### `betterAuthStatelessSession` has no machine path
191
+
192
+ The lean cookie-cache middleware (`betterAuthStatelessSession` — no
193
+ `services.auth()`, no DB) handles only the human path. Machine auth needs
194
+ `betterAuthSession`, because `verifyApiKey` is a server call there is no
195
+ stateless equivalent of. Both accept an `impersonation` option.
161
196
 
162
197
  ### WebSocket channels authenticate on the upgrade handshake
163
198
 
@@ -2,11 +2,11 @@
2
2
  name: pikku-mcp
3
3
  description: >-
4
4
  Use when exposing Pikku functions as MCP tools, resources, or prompts for AI assistants. Covers
5
- mcp: true flag, pikkuMCPResourceFunc, pikkuMCPPromptFunc, and MCP wire object. TRIGGER when:
6
- code uses mcp: true or pikkuMCPResourceFunc/pikkuMCPPromptFunc, user asks about MCP, Model
7
- Context Protocol, AI tool integration, or exposing functions to Claude/ChatGPT. DO NOT TRIGGER
8
- when: user asks about AI agents (use pikku-ai-agent) or general function definitions (use
9
- pikku-concepts).
5
+ mcp: true, pikkuMCPToolFunc, pikkuMCPResourceFunc, pikkuMCPPromptFunc, wireMCPResource,
6
+ wireMCPPrompt, the MCP wire object and PikkuMCPServer. TRIGGER when: code uses mcp: true or any
7
+ pikkuMCP*Func/wireMCP* helper, user asks about MCP, Model Context Protocol, AI tool integration,
8
+ or exposing functions to Claude/ChatGPT. DO NOT TRIGGER when: user asks about AI agents (use
9
+ pikku-ai-agent) or general function definitions (use pikku-concepts).
10
10
  installGroups: [core]
11
11
  ---
12
12
 
@@ -33,209 +33,219 @@ pikku info tags --verbose # Understand project organization
33
33
 
34
34
  See `pikku-concepts` for the core mental model.
35
35
 
36
+ ## The shape of MCP in Pikku
37
+
38
+ MCP has three surfaces, and Pikku wires them differently:
39
+
40
+ | Surface | Function factory | Wiring | Return type |
41
+ | --- | --- | --- | --- |
42
+ | **Tool** | `mcp: true` on a `pikkuFunc`, or `pikkuMCPToolFunc` | none — the function *is* the registration | the func's own output, or MCP content blocks |
43
+ | **Resource** | `pikkuMCPResourceFunc` | `wireMCPResource({ uri, title, … })` | `Array<{ uri, text }>` |
44
+ | **Prompt** | `pikkuMCPPromptFunc` | `wireMCPPrompt({ name, description, … })` | `Array<MCPPromptMessage>` |
45
+
46
+ Tools are the odd one out — there is no `wireMCPTool`. Resources and prompts
47
+ carry protocol metadata (a URI template, a prompt name) that belongs to the
48
+ endpoint rather than the implementation, so that metadata lives on the wiring and
49
+ the `pikkuMCP*Func` factory stays a plain function.
50
+
51
+ Import every factory and wiring from `#pikku`.
52
+
36
53
  ## API Reference
37
54
 
38
- ### MCP Tools (simplest approach)
55
+ ### Tools
39
56
 
40
- Add `mcp: true` to any existing `pikkuFunc` to expose it as an MCP tool:
57
+ Add `mcp: true` to any existing function:
41
58
 
42
59
  ```typescript
43
- const myFunc = pikkuFunc({
44
- description: string, // Used as MCP tool description
45
- input: ZodSchema, // Becomes MCP tool input schema
46
- output: ZodSchema, // Return type
47
- mcp: true, // ← Expose as MCP tool
48
- func: async (services, data) => { ... },
60
+ export const createTodo = pikkuFunc({
61
+ description: 'Create a new todo item', // becomes the MCP tool description
62
+ input: CreateTodoInput, // becomes the MCP tool input schema
63
+ output: CreateTodoOutput,
64
+ mcp: true,
65
+ func: async ({ db }, { text, priority }) => db.createTodo({ text, priority }),
49
66
  })
50
67
  ```
51
68
 
52
- ### MCP Resources (`pikkuMCPResourceFunc`)
69
+ A missing `description` is all an assistant has to go on, so codegen warns about
70
+ it rather than failing — treat the warning as a bug.
71
+
72
+ Use `pikkuMCPToolFunc` when the tool should control its own presentation. It
73
+ returns MCP content blocks (`{ type: 'text', text }` or `{ type: 'image', data }`
74
+ with base64), so the assistant reads prose rather than raw JSON:
53
75
 
54
76
  ```typescript
55
- import { pikkuMCPResourceFunc } from '#pikku'
77
+ import { pikkuMCPToolFunc } from '#pikku'
56
78
 
57
- const resource = pikkuMCPResourceFunc({
58
- uri: string, // URI template, e.g. 'todos/{id}'
59
- title: string, // Human-readable title
60
- description?: string,
61
- func: async (services, data, { mcp }) => {
62
- // Must return array of { uri, text } or { uri, blob, mimeType }
63
- return [{ uri: mcp.uri!, text: JSON.stringify(result) }]
79
+ export const createTodoTool = pikkuMCPToolFunc({
80
+ description: 'Create a todo item with title, priority, due date and tags',
81
+ input: CreateTodoWithUserInputSchema,
82
+ func: async (_services, input, { rpc }) => {
83
+ const { todo } = await rpc.invoke('createTodo', input)
84
+ return [
85
+ { type: 'text' as const, text: `Created "${todo.title}" (${todo.id})` },
86
+ ]
64
87
  },
65
88
  })
66
89
  ```
67
90
 
68
- ### MCP Prompts (`pikkuMCPPromptFunc`)
91
+ It also accepts `name`, `title`, `summary`, `tags`, `middleware` and
92
+ `permissions`. The function is sessionless and gets `mcp` and `rpc` on its wire —
93
+ calling existing business functions through `rpc.invoke` keeps the tool a thin
94
+ presentation layer over logic that is already tested and reachable over HTTP.
95
+
96
+ ### Resources
69
97
 
70
98
  ```typescript
71
- import { pikkuMCPPromptFunc } from '#pikku'
99
+ import { pikkuMCPResourceFunc } from '#pikku'
72
100
 
73
- const prompt = pikkuMCPPromptFunc({
74
- name: string,
75
- description: string,
76
- func: async (services, data) => {
77
- // Must return array of MCP messages
101
+ export const getTodoResource = pikkuMCPResourceFunc<{ id: string }>(
102
+ async (_services, { id }, { rpc, mcp }) => {
103
+ const { todo } = await rpc.invoke('getTodo', { id })
78
104
  return [
79
105
  {
80
- role: 'user',
81
- content: { type: 'text', text: '...' },
106
+ uri: mcp.uri!,
107
+ text: todo ? formatTodo(todo) : `Todo "${id}" not found.`,
82
108
  },
83
109
  ]
84
- },
85
- })
86
- ```
87
-
88
- ### MCP Wire Object
89
-
90
- Inside MCP-enabled functions, `wire.mcp` provides:
91
-
92
- ```typescript
93
- mcp.uri // Current resource URI (for resources)
94
- mcp.sendResourceUpdated(uri) // Notify clients a resource changed
95
- mcp.enableTools({ toolName: true }) // Dynamically enable/disable tools
110
+ }
111
+ )
96
112
  ```
97
113
 
98
- ## Usage Patterns
99
-
100
- ### Expose Existing Functions as MCP Tools
101
-
102
- The simplest path — add `mcp: true` to any function:
114
+ The factory takes either a bare function (as above) or a config object — `{ func, name }`,
115
+ or `{ func, input }` with a schema. A resource returns `Array<{ uri, text }>`;
116
+ it is text only, with no blob variant. `mcp.uri` is the concrete URI the client
117
+ asked for, which is why each entry echoes it back.
103
118
 
104
119
  ```typescript
105
- export const createTodo = pikkuFunc({
106
- description: 'Create a new todo item',
107
- input: CreateTodoInput,
108
- output: CreateTodoOutput,
109
- mcp: true,
110
- func: async ({ db }, { text, priority }) => {
111
- return await db.createTodo({ text, priority })
112
- },
113
- })
114
- ```
120
+ import { wireMCPResource } from '#pikku'
115
121
 
116
- ### MCP Resources with URI Templates
117
-
118
- ```typescript
119
- export const getTodo = pikkuMCPResourceFunc({
120
- uri: 'todos/{id}',
122
+ wireMCPResource({
123
+ uri: 'todos/{id}', // URI template
121
124
  title: 'Todo Details',
122
- description: 'Get a todo by ID',
123
- func: async ({ db }, { id }, { mcp }) => {
124
- const todo = await db.getTodo(id)
125
- return [{ uri: mcp.uri!, text: JSON.stringify(todo) }]
126
- },
125
+ description: 'Get details of a specific todo by ID',
126
+ func: getTodoResource,
127
+ tags: ['todos'],
128
+ // also: summary?, mimeType?, size?, streaming?, errors?, middleware?
127
129
  })
128
130
  ```
129
131
 
130
- ### MCP Prompts
132
+ Every `{param}` in `uri` is checked against the function's input at compile time,
133
+ so `todos/{id}` wired to a function whose input has no `id` fails to build rather
134
+ than handing the function an `undefined`.
135
+
136
+ ### Prompts
131
137
 
132
138
  ```typescript
133
- export const codeReview = pikkuMCPPromptFunc({
134
- name: 'codeReview',
135
- description: 'Generate a code review prompt',
136
- func: async ({}, { filePath, context }) => {
139
+ import { pikkuMCPPromptFunc, wireMCPPrompt } from '#pikku'
140
+
141
+ export const planDayPrompt = pikkuMCPPromptFunc({
142
+ input: UserIdInputSchema,
143
+ func: async (_services, { userId }, { rpc }) => {
144
+ const { todos } = await rpc.invoke('listTodos', { userId, completed: false })
137
145
  return [
138
146
  {
139
- role: 'user',
147
+ role: 'user' as const,
140
148
  content: {
141
- type: 'text',
142
- text: `Review ${filePath}. Context: ${context}`,
149
+ type: 'text' as const,
150
+ text: `Plan my day:\n${todos.map(formatTodo).join('\n')}`,
143
151
  },
144
152
  },
145
153
  ]
146
154
  },
147
155
  })
156
+
157
+ wireMCPPrompt({
158
+ name: 'planDay',
159
+ description: 'Generate a daily plan based on pending todos',
160
+ func: planDayPrompt,
161
+ tags: ['productivity'],
162
+ })
148
163
  ```
149
164
 
150
- ### Dynamic Tool Control
165
+ A message's `role` is `'user' | 'assistant' | 'system'` and its `content.type` is
166
+ `'text' | 'image'`. The prompt arguments the client sees are derived from the
167
+ input schema at codegen time: each property becomes a named argument, and
168
+ schema-required properties become required arguments.
169
+
170
+ ### MCP Wire Object
171
+
172
+ Available as `wire.mcp` inside any MCP function:
173
+
174
+ ```typescript
175
+ mcp.uri // the resolved resource URI (resources only)
176
+ mcp.sendResourceUpdated(uri) // notify clients a resource changed
177
+ await mcp.enableTools({ archiveTodos: true })
178
+ await mcp.enableResources({ todoDetails: false })
179
+ await mcp.enablePrompts({ planDay: true })
180
+ ```
181
+
182
+ The `enable*` calls are how a server presents a changing surface — hiding tools
183
+ that are meaningless in the current state beats letting the assistant call them
184
+ and fail. Each returns a boolean, and each name is typechecked against your
185
+ generated endpoint names.
151
186
 
152
187
  ```typescript
153
- export const manageTodos = pikkuFunc({
154
- description: 'Manage todo items',
155
- input: ManageTodosInput,
156
- output: ManageTodosOutput,
188
+ export const deleteTodo = pikkuFunc({
189
+ description: 'Delete a todo item',
157
190
  mcp: true,
158
- func: async ({ db }, { action, id }, { mcp }) => {
159
- if (action === 'delete') {
160
- await db.deleteTodo(id)
161
- mcp.sendResourceUpdated(`todos/${id}`)
162
- await mcp.enableTools({ archiveTodos: true })
163
- return { deleted: true }
164
- }
191
+ func: async ({ db }, { id }, { mcp }) => {
192
+ await db.deleteTodo(id)
193
+ mcp.sendResourceUpdated(`todos/${id}`)
194
+ return { deleted: true }
165
195
  },
166
196
  })
167
197
  ```
168
198
 
169
- ### MCP Server Setup
199
+ ## MCP Server Setup
200
+
201
+ `PikkuMCPServer` takes the server config and a logger — not your services. It
202
+ loads the generated `mcp.gen.json`, and the bootstrap import is what registers
203
+ your functions.
170
204
 
171
205
  ```typescript
172
206
  // start.ts
173
207
  import { PikkuMCPServer } from '@pikku/modelcontextprotocol'
208
+ import { createConfig, createSingletonServices } from './services.js'
209
+ import mcpJSON from '../.pikku/mcp/mcp.gen.json' with { type: 'json' }
210
+ import '../.pikku/pikku-bootstrap.gen.js'
211
+
212
+ const config = await createConfig()
213
+ const singletonServices = await createSingletonServices(config)
214
+
215
+ const server = new PikkuMCPServer(
216
+ {
217
+ name: 'pikku-mcp-server',
218
+ version: '1.0.0',
219
+ mcpJSON,
220
+ capabilities: { logging: {}, tools: {}, resources: {}, prompts: {} },
221
+ },
222
+ singletonServices.logger
223
+ )
174
224
 
175
- const server = new PikkuMCPServer(config, singletonServices, createWireServices)
176
225
  await server.init()
177
- await server.start()
178
- ```
179
226
 
180
- ## Complete Example
227
+ // stdio — the transport desktop MCP clients spawn
228
+ await server.connectStdio()
229
+ singletonServices.logger = server.createMCPLogger()
181
230
 
182
- ```typescript
183
- // functions/todos.functions.ts
184
- export const listTodos = pikkuSessionlessFunc({
185
- description: 'List all todo items',
186
- input: ListTodosInput,
187
- output: ListTodosOutput,
188
- mcp: true,
189
- func: async ({ db }, { status }) => {
190
- return { todos: await db.listTodos(status) }
191
- },
192
- })
231
+ // …or streamable HTTP, for a hosted server
232
+ const { close } = await server.connectHTTP({ port: 3000, host: '127.0.0.1' })
233
+ ```
193
234
 
194
- export const createTodo = pikkuFunc({
195
- description: 'Create a new todo item',
196
- input: CreateTodoInput,
197
- output: CreateTodoOutput,
198
- mcp: true,
199
- func: async ({ db }, { text, priority }) => {
200
- return await db.createTodo({ text, priority })
201
- },
202
- })
235
+ `capabilities` is a filter, not documentation: a surface you leave out is not
236
+ advertised and its endpoints are never loaded, which is how you ship a tools-only
237
+ server.
203
238
 
204
- export const completeTodo = pikkuFunc({
205
- description: 'Mark a todo as complete',
206
- input: CompleteTodoInput,
207
- output: CompleteTodoOutput,
208
- mcp: true,
209
- func: async ({ db }, { todoId }) => {
210
- return await db.completeTodo(todoId)
211
- },
212
- })
239
+ Over stdio the protocol owns stdout, so an ordinary console logger corrupts the
240
+ frames — that is what `createMCPLogger()` is for. Swap the logger before
241
+ anything logs.
213
242
 
214
- // functions/todos.mcp.ts
215
- export const getTodoResource = pikkuMCPResourceFunc({
216
- uri: 'todos/{id}',
217
- title: 'Todo Details',
218
- description: 'Get details of a specific todo',
219
- func: async ({ db }, { id }, { mcp }) => {
220
- const todo = await db.getTodo(id)
221
- return [{ uri: mcp.uri!, text: JSON.stringify(todo) }]
222
- },
223
- })
243
+ ## Red flags
224
244
 
225
- export const planDayPrompt = pikkuMCPPromptFunc({
226
- name: 'planDay',
227
- description: 'Create a daily plan based on pending todos',
228
- func: async ({ db }, {}) => {
229
- const { todos } = await db.listTodos('pending')
230
- return [
231
- {
232
- role: 'user',
233
- content: {
234
- type: 'text',
235
- text: `Plan my day. Here are my pending todos:\n${todos.map((t) => `- ${t.text} (${t.priority})`).join('\n')}`,
236
- },
237
- },
238
- ]
239
- },
240
- })
241
- ```
245
+ | Symptom | Cause |
246
+ | --- | --- |
247
+ | `wireMCPTool` is not exported | There is no tool wiring — use `mcp: true` or `pikkuMCPToolFunc` |
248
+ | `uri`/`title` rejected on `pikkuMCPResourceFunc` | Those belong on `wireMCPResource` |
249
+ | Resource returning `{ uri, blob, mimeType }` | Resources are text only: `{ uri, text }` |
250
+ | Client sees a tool with no description | `mcp: true` without a `description` — check the codegen warning |
251
+ | stdio client disconnects on the first log line | Logger still writing to stdout; use `createMCPLogger()` |
@@ -136,7 +136,7 @@ Tags from the function definition and the wire object are merged — middleware
136
136
  ### Registering Tag Middleware
137
137
 
138
138
  ```typescript
139
- import { addTagMiddleware } from '.pikku/pikku-types.gen.js'
139
+ import { addTagMiddleware } from '#pikku'
140
140
 
141
141
  addTagMiddleware('machine-agent', [machineAgentBearerAuth])
142
142
  ```
@@ -145,18 +145,30 @@ Call at module load time — typically in the same `wirings/*.ts` file as the `w
145
145
 
146
146
  ## Middleware Execution Order
147
147
 
148
- **Scope resolution order (broadest → narrowest):**
148
+ Resolution happens in two steps, and the order matters more than it looks.
149
+
150
+ **Step 1 — collect, broadest → narrowest:**
149
151
 
150
152
  ```text
151
153
  global → httpGroup/* → httpGroup/prefix → wiringTags → wiringMiddleware → funcTags → funcMiddleware → function body
152
154
  ```
153
155
 
154
- **Within each scope, sorted by priority:**
156
+ **Step 2 — sort that whole flat list by priority:**
155
157
 
156
158
  ```text
157
159
  highest → high → medium (default) → low → lowest
158
160
  ```
159
161
 
162
+ **Priority is the primary key across every scope, not within one.** The collected
163
+ list is flattened first and sorted once, so a `priority: 'lowest'` global
164
+ middleware runs *after* an inline per-route middleware of default priority — the
165
+ narrower scope does not win. Scope order survives only as the tiebreaker between
166
+ middleware of equal priority, because the sort is stable.
167
+
168
+ This is what makes `telemetryOuter`/`telemetryInner` work: they pin themselves to
169
+ `highest`/`lowest` so they bracket every other middleware no matter where those
170
+ were registered.
171
+
160
172
  Set priority using the config-object form of `pikkuMiddleware`:
161
173
 
162
174
  ```typescript
@@ -167,7 +179,7 @@ const earlyMiddleware = pikkuMiddleware({
167
179
  })
168
180
  ```
169
181
 
170
- Priority is the primary sort key; within the same level, registration order is preserved. Use priority when a middleware must run before/after others regardless of registration order (e.g. telemetry wrapping everything, session extraction before auth checks).
182
+ Within the same priority level, the collection order above is preserved. Use priority when a middleware must run before/after others regardless of where it was registered (e.g. telemetry wrapping everything, session extraction before auth checks).
171
183
 
172
184
  ## Service-to-Service Bearer Auth (canonical pattern)
173
185
 
@@ -185,7 +197,7 @@ export const getToken = () => _token
185
197
  ```typescript
186
198
  // wirings/http.wiring.ts
187
199
  import { timingSafeEqual } from 'node:crypto'
188
- import { addTagMiddleware, pikkuMiddleware } from '../../.pikku/pikku-types.gen.js'
200
+ import { addTagMiddleware, pikkuMiddleware } from '#pikku'
189
201
  import { UnauthorizedError } from '@pikku/core/errors'
190
202
  import { getToken } from '../lib/host-token.js'
191
203