@pikku/skills 0.12.4 → 0.12.8
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 +74 -33
- 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-knowledge/SKILL.md +50 -7
- 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 +141 -76
- 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 +123 -11
- 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
|
@@ -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
|
|
|
@@ -59,17 +59,25 @@ await mongo.close()
|
|
|
59
59
|
| `MongoDBAIStorageService` | `AIStorageService, AIRunStateService` | AI conversation/run storage |
|
|
60
60
|
| `MongoDBAgentRunService` | `AgentRunService` | Agent execution tracking |
|
|
61
61
|
| `MongoDBSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
|
|
62
|
+
| `MongoDBSessionStore` | `SessionStore` | Persisted user sessions |
|
|
62
63
|
|
|
63
64
|
All services take a `Db` instance in their constructor and have an `init()` method that creates collections/indexes.
|
|
64
65
|
|
|
65
66
|
### Secret Service
|
|
66
67
|
|
|
68
|
+
Envelope encryption: `key` derives the KEK that wraps each secret's own DEK.
|
|
69
|
+
Keeping `previousKey` set is what makes `rotateKEK()` possible — it re-wraps
|
|
70
|
+
every secret onto the current key and returns the new version.
|
|
71
|
+
|
|
67
72
|
```typescript
|
|
68
73
|
import { MongoDBSecretService } from '@pikku/mongodb'
|
|
69
74
|
|
|
70
75
|
const secrets = new MongoDBSecretService(mongo.db, {
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
key: 'your-key-encryption-passphrase',
|
|
77
|
+
keyVersion: 2, // defaults to 1
|
|
78
|
+
previousKey: 'the-passphrase-you-are-rotating-away-from',
|
|
79
|
+
audit: true, // log write/delete/rotate through the audit sink
|
|
80
|
+
auditReads: false, // reads too — noisy, off by default
|
|
73
81
|
})
|
|
74
82
|
await secrets.init()
|
|
75
83
|
|
|
@@ -35,16 +35,24 @@ missing dependency).
|
|
|
35
35
|
### 1 — Run the importer (do as much as possible, cheaply)
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
pikku import n8n <
|
|
38
|
+
pikku import n8n <file> [--out <dir>] # -o for short
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
The output directory is an **option**, not a positional argument; omitted, it
|
|
42
|
+
falls back to `scaffold.functionDir` from `pikku.config.json`, then cwd.
|
|
43
|
+
|
|
44
|
+
`<file>` is one export, **or a directory** — the command reads every `.json` in
|
|
45
|
+
it — and either form may hold a single workflow object, a bare array (`n8n
|
|
46
|
+
export:workflow --all`), or a `{ workflows: [...] }` wrapper. All of those are
|
|
47
|
+
flattened into one import per workflow, so there is no need to loop yourself.
|
|
48
|
+
|
|
41
49
|
It writes `<slug>.graph.ts` (+ `.agent.ts` for AI workflows), `<slug>.addons.gen.ts`,
|
|
42
50
|
a `<slug>.integrations.json` manifest, and one stub function per node it could not
|
|
43
|
-
map.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
map. An un-importable workflow (a cross-workflow sub-workflow reference, a dynamic
|
|
52
|
+
workflow target, a mid-flow `respondToWebhook`) is reported as `[reason] message`
|
|
53
|
+
and **skipped** — nothing partial is written for it. Across a batch the others
|
|
54
|
+
still import; the command exits 1 at the end if any failed, so read the log rather
|
|
55
|
+
than the exit code to know what landed. Relay every skipped workflow to the user.
|
|
48
56
|
|
|
49
57
|
### 2 — Triage what it left
|
|
50
58
|
|