@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.
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +56 -29
- package/skills/pikku-ai-agent/SKILL.md +197 -105
- package/skills/pikku-ai-vercel/SKILL.md +57 -18
- package/skills/pikku-ai-voice/SKILL.md +126 -52
- package/skills/pikku-audit/SKILL.md +35 -13
- package/skills/pikku-aws/SKILL.md +66 -16
- package/skills/pikku-backblaze/SKILL.md +44 -11
- package/skills/pikku-better-auth/SKILL.md +45 -10
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +75 -10
- package/skills/pikku-config/SKILL.md +56 -14
- package/skills/pikku-cron/SKILL.md +13 -6
- package/skills/pikku-deploy-azure/SKILL.md +83 -28
- package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
- package/skills/pikku-deploy-express/SKILL.md +40 -4
- package/skills/pikku-deploy-fastify/SKILL.md +22 -1
- package/skills/pikku-deploy-lambda/SKILL.md +99 -19
- package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
- package/skills/pikku-deploy-uws/SKILL.md +54 -1
- package/skills/pikku-deps/SKILL.md +29 -8
- package/skills/pikku-emails/SKILL.md +36 -5
- package/skills/pikku-fabric/SKILL.md +27 -2
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +6 -1
- package/skills/pikku-gateway-slack/SKILL.md +72 -11
- package/skills/pikku-http/SKILL.md +18 -5
- package/skills/pikku-http/references/http-options.md +10 -5
- package/skills/pikku-i18n/SKILL.md +18 -7
- package/skills/pikku-info/SKILL.md +18 -8
- package/skills/pikku-jose/SKILL.md +35 -6
- package/skills/pikku-kysely/SKILL.md +78 -15
- package/skills/pikku-machine-auth/SKILL.md +36 -1
- package/skills/pikku-mcp/SKILL.md +159 -149
- package/skills/pikku-middleware/SKILL.md +17 -5
- package/skills/pikku-mongodb/SKILL.md +10 -2
- package/skills/pikku-n8n-import/SKILL.md +14 -6
- package/skills/pikku-permissions/SKILL.md +102 -22
- package/skills/pikku-pino/SKILL.md +12 -4
- package/skills/pikku-product-second-opinion/SKILL.md +3 -3
- package/skills/pikku-queue/SKILL.md +45 -16
- package/skills/pikku-react/SKILL.md +41 -14
- package/skills/pikku-react-query/SKILL.md +14 -10
- package/skills/pikku-realtime/SKILL.md +44 -22
- package/skills/pikku-redis/SKILL.md +12 -3
- package/skills/pikku-rpc/SKILL.md +23 -12
- package/skills/pikku-rtl/SKILL.md +21 -17
- package/skills/pikku-scenario/SKILL.md +108 -75
- package/skills/pikku-schedule/SKILL.md +39 -6
- package/skills/pikku-schema-ajv/SKILL.md +24 -2
- package/skills/pikku-schema-cfworker/SKILL.md +22 -2
- package/skills/pikku-security/SKILL.md +54 -9
- package/skills/pikku-services/SKILL.md +49 -9
- package/skills/pikku-services/references/audit-wire-service.md +2 -1
- package/skills/pikku-template-clone/SKILL.md +10 -5
- package/skills/pikku-trigger/SKILL.md +50 -6
- package/skills/pikku-versioning/SKILL.md +46 -17
- package/skills/pikku-websocket/SKILL.md +72 -44
- package/skills/pikku-workflow/SKILL.md +35 -1
- package/skills/pikku-workflow/references/workflow-reference.md +13 -8
- package/skills/pikku-workflows-client/SKILL.md +13 -6
- 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.
|
|
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
|
|
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
|
|
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
|
|
109
|
-
yarn add @pikku/kysely @pikku/kysely-mysql
|
|
110
|
-
yarn add @pikku/kysely @pikku/kysely-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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
|
169
|
-
await secrets.
|
|
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
|
|
6
|
-
code uses mcp: true or
|
|
7
|
-
|
|
8
|
-
when: user asks about AI agents (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
|
-
###
|
|
55
|
+
### Tools
|
|
39
56
|
|
|
40
|
-
Add `mcp: true` to any existing
|
|
57
|
+
Add `mcp: true` to any existing function:
|
|
41
58
|
|
|
42
59
|
```typescript
|
|
43
|
-
const
|
|
44
|
-
description:
|
|
45
|
-
input:
|
|
46
|
-
output:
|
|
47
|
-
mcp: true,
|
|
48
|
-
func: async (
|
|
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
|
-
|
|
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 {
|
|
77
|
+
import { pikkuMCPToolFunc } from '#pikku'
|
|
56
78
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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 {
|
|
99
|
+
import { pikkuMCPResourceFunc } from '#pikku'
|
|
72
100
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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: `
|
|
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
|
-
|
|
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
|
|
154
|
-
description: '
|
|
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 }, {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
+
// stdio — the transport desktop MCP clients spawn
|
|
228
|
+
await server.connectStdio()
|
|
229
|
+
singletonServices.logger = server.createMCPLogger()
|
|
181
230
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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 '
|
|
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
|
|