@pikku/core 0.12.20 → 0.12.22

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 (141) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/dev/hot-reload.js +20 -6
  3. package/dist/errors/error-handler.d.ts +1 -1
  4. package/dist/errors/error-handler.js +2 -2
  5. package/dist/function/functions.types.d.ts +3 -2
  6. package/dist/handle-error.js +3 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/middleware/index.d.ts +2 -2
  10. package/dist/middleware/index.js +2 -2
  11. package/dist/middleware/telemetry.d.ts +2 -2
  12. package/dist/middleware/telemetry.js +2 -2
  13. package/dist/middleware-runner.d.ts +15 -27
  14. package/dist/middleware-runner.js +25 -30
  15. package/dist/permissions.d.ts +15 -22
  16. package/dist/permissions.js +30 -25
  17. package/dist/pikku-request.js +1 -1
  18. package/dist/pikku-state.js +2 -3
  19. package/dist/services/ai-agent-runner-service.d.ts +1 -0
  20. package/dist/services/in-memory-queue-service.js +1 -1
  21. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  22. package/dist/services/in-memory-workflow-service.js +31 -13
  23. package/dist/services/local-content.js +8 -1
  24. package/dist/services/user-session-service.d.ts +1 -1
  25. package/dist/testing/service-tests.js +24 -0
  26. package/dist/types/core.types.d.ts +4 -0
  27. package/dist/types/state.types.d.ts +2 -18
  28. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  29. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  30. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  31. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  32. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  33. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  34. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  35. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  36. package/dist/wirings/channel/channel-handler.js +1 -1
  37. package/dist/wirings/channel/channel-store.d.ts +13 -0
  38. package/dist/wirings/channel/channel.types.d.ts +3 -0
  39. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  40. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  41. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  42. package/dist/wirings/cli/cli-runner.js +24 -5
  43. package/dist/wirings/cli/command-parser.js +19 -4
  44. package/dist/wirings/http/http-runner.js +72 -36
  45. package/dist/wirings/http/http.types.d.ts +1 -0
  46. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  47. package/dist/wirings/http/web-request.js +32 -0
  48. package/dist/wirings/rpc/rpc-runner.js +13 -3
  49. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  50. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  51. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  52. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  53. package/dist/wirings/workflow/index.d.ts +1 -1
  54. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  55. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  56. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  57. package/package.json +1 -1
  58. package/run-tests.sh +4 -1
  59. package/src/dev/hot-reload.test.ts +62 -11
  60. package/src/dev/hot-reload.ts +28 -7
  61. package/src/errors/error-handler.ts +8 -2
  62. package/src/errors/error.test.ts +2 -0
  63. package/src/function/function-runner.test.ts +500 -10
  64. package/src/function/functions.types.ts +3 -2
  65. package/src/handle-error.test.ts +1 -1
  66. package/src/handle-error.ts +4 -1
  67. package/src/index.ts +12 -2
  68. package/src/middleware/index.ts +11 -2
  69. package/src/middleware/telemetry.ts +2 -2
  70. package/src/middleware-runner.test.ts +16 -16
  71. package/src/middleware-runner.ts +42 -30
  72. package/src/permissions.test.ts +27 -24
  73. package/src/permissions.ts +41 -25
  74. package/src/pikku-request.test.ts +35 -0
  75. package/src/pikku-request.ts +1 -1
  76. package/src/pikku-state.ts +2 -3
  77. package/src/run-tests-script.test.ts +18 -0
  78. package/src/services/ai-agent-runner-service.ts +1 -0
  79. package/src/services/in-memory-queue-service.ts +1 -1
  80. package/src/services/in-memory-session-store.ts +1 -2
  81. package/src/services/in-memory-workflow-service.test.ts +33 -11
  82. package/src/services/in-memory-workflow-service.ts +49 -13
  83. package/src/services/local-content.test.ts +54 -0
  84. package/src/services/local-content.ts +12 -1
  85. package/src/services/typed-credential-service.ts +3 -3
  86. package/src/services/typed-secret-service.ts +3 -3
  87. package/src/services/typed-variables-service.ts +3 -3
  88. package/src/services/user-session-service.ts +4 -4
  89. package/src/testing/service-tests.ts +30 -0
  90. package/src/types/core.types.ts +6 -2
  91. package/src/types/state.types.ts +2 -13
  92. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  93. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  94. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  95. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  96. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  97. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  98. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  99. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  100. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  101. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  102. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  103. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  104. package/src/wirings/channel/channel-handler.test.ts +272 -0
  105. package/src/wirings/channel/channel-handler.ts +1 -1
  106. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  107. package/src/wirings/channel/channel-store.ts +19 -0
  108. package/src/wirings/channel/channel.types.ts +4 -0
  109. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  110. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  111. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  112. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  113. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  114. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  115. package/src/wirings/cli/cli-runner.test.ts +255 -2
  116. package/src/wirings/cli/cli-runner.ts +31 -10
  117. package/src/wirings/cli/cli.types.ts +4 -8
  118. package/src/wirings/cli/command-parser.test.ts +83 -0
  119. package/src/wirings/cli/command-parser.ts +23 -4
  120. package/src/wirings/http/http-runner.test.ts +296 -1
  121. package/src/wirings/http/http-runner.ts +87 -57
  122. package/src/wirings/http/http.types.ts +11 -26
  123. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  124. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  125. package/src/wirings/http/web-request.test.ts +115 -0
  126. package/src/wirings/http/web-request.ts +43 -5
  127. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  128. package/src/wirings/queue/queue.types.ts +2 -5
  129. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  130. package/src/wirings/rpc/rpc-runner.ts +15 -3
  131. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  132. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  133. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  134. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  135. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  136. package/src/wirings/workflow/index.ts +1 -0
  137. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  138. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  139. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  140. package/src/wirings/workflow/workflow.types.ts +68 -5
  141. package/tsconfig.tsbuildinfo +1 -1
@@ -154,6 +154,42 @@ describe('toWebRequest', () => {
154
154
  assert.equal(text, bodyContent)
155
155
  })
156
156
 
157
+ test('reconstructs JSON body from pre-parsed request data when arrayBuffer is empty', async () => {
158
+ const req = createMockRequest({
159
+ method: 'post',
160
+ path: '/json-body',
161
+ headers: { 'content-type': 'application/json', host: 'localhost' },
162
+ body: new ArrayBuffer(0),
163
+ })
164
+
165
+ req.json = async () => ({ hello: 'world' })
166
+
167
+ const webReq = toWebRequest(req)
168
+
169
+ assert.equal(await webReq.text(), '{"hello":"world"}')
170
+ })
171
+
172
+ test('reconstructs form-urlencoded body from pre-parsed request data when arrayBuffer is empty', async () => {
173
+ const req = createMockRequest({
174
+ method: 'post',
175
+ path: '/form-body',
176
+ headers: {
177
+ 'content-type': 'application/x-www-form-urlencoded',
178
+ host: 'localhost',
179
+ },
180
+ body: new ArrayBuffer(0),
181
+ })
182
+
183
+ req.json = async () => ({ name: 'Alice', role: 'admin' })
184
+
185
+ const webReq = toWebRequest(req)
186
+ const text = await webReq.text()
187
+
188
+ assert.ok(
189
+ text === 'name=Alice&role=admin' || text === 'role=admin&name=Alice'
190
+ )
191
+ })
192
+
157
193
  test('GET request has no body', async () => {
158
194
  const req = createMockRequest({ method: 'get' })
159
195
  const webReq = toWebRequest(req)
@@ -233,4 +269,83 @@ describe('applyWebResponse', () => {
233
269
  assert.equal(state.body, null)
234
270
  assert.equal(state.statusCode, 204)
235
271
  })
272
+
273
+ test('collects multiple set-cookie headers via getSetCookie without duplicates', async () => {
274
+ const { res, state } = createMockResponse()
275
+ const webRes = new Response('{"csrfToken":"abc"}', { status: 200 })
276
+ const cookies = [
277
+ 'authjs.csrf-token=abc; Path=/; HttpOnly',
278
+ 'authjs.callback-url=http%3A%2F%2Flocalhost; Path=/; HttpOnly',
279
+ ]
280
+ ;(webRes.headers as any).getSetCookie = () => cookies
281
+
282
+ await applyWebResponse(res, webRes)
283
+
284
+ assert.deepEqual(state.headers['set-cookie'], cookies)
285
+ assert.equal(state.body, '{"csrfToken":"abc"}')
286
+ })
287
+
288
+ test('falls back to forEach for set-cookie when getSetCookie is absent', async () => {
289
+ const { res, state } = createMockResponse()
290
+ const headers = new Headers()
291
+ headers.append('Set-Cookie', 'a=1; Path=/')
292
+ headers.append('Set-Cookie', 'b=2; Path=/')
293
+ const webRes = new Response(null, { status: 200, headers })
294
+ delete (webRes.headers as any).getSetCookie
295
+
296
+ await applyWebResponse(res, webRes)
297
+
298
+ const setCookies = state.headers['set-cookie'] as string[]
299
+ assert.ok(setCookies.some((c) => c.includes('a=1')))
300
+ assert.ok(setCookies.some((c) => c.includes('b=2')))
301
+ })
302
+
303
+ test('uses res.send when available and skips transport headers', async () => {
304
+ const sent: string[] = []
305
+ const { state } = createMockResponse()
306
+ const res: PikkuHTTPResponse = {
307
+ status(code: number) {
308
+ state.statusCode = code
309
+ return res
310
+ },
311
+ header(name: string, value: string | string[]) {
312
+ state.headers[name.toLowerCase()] = value
313
+ return res
314
+ },
315
+ cookie() {
316
+ return res
317
+ },
318
+ json() {
319
+ return res
320
+ },
321
+ arrayBuffer() {
322
+ throw new Error('arrayBuffer should not be used when send exists')
323
+ },
324
+ send(data: string | ArrayBuffer | Buffer) {
325
+ sent.push(String(data))
326
+ return res
327
+ },
328
+ redirect(location: string, status?: number) {
329
+ state.redirectUrl = location
330
+ state.redirectStatus = status ?? 302
331
+ return res
332
+ },
333
+ }
334
+
335
+ const webRes = new Response('payload', {
336
+ status: 200,
337
+ headers: {
338
+ 'content-length': '7',
339
+ 'transfer-encoding': 'chunked',
340
+ 'x-custom': 'ok',
341
+ },
342
+ })
343
+
344
+ await applyWebResponse(res, webRes)
345
+
346
+ assert.deepEqual(sent, ['payload'])
347
+ assert.equal(state.headers['x-custom'], 'ok')
348
+ assert.equal(state.headers['content-length'], undefined)
349
+ assert.equal(state.headers['transfer-encoding'], undefined)
350
+ })
236
351
  })
@@ -7,11 +7,9 @@ import type { PikkuHTTPResponse } from './http.types.js'
7
7
  */
8
8
  export function toWebRequest(req: PikkuHTTPRequest, baseUrl?: string): Request {
9
9
  const proto = req.header('x-forwarded-proto') ?? 'http'
10
- const host = req.header('x-forwarded-host') ?? req.header('host') ?? 'localhost'
11
- const url = new URL(
12
- req.path(),
13
- baseUrl ?? `${proto}://${host}`
14
- )
10
+ const host =
11
+ req.header('x-forwarded-host') ?? req.header('host') ?? 'localhost'
12
+ const url = new URL(req.path(), baseUrl ?? `${proto}://${host}`)
15
13
 
16
14
  const query = req.query()
17
15
  for (const [key, value] of Object.entries(query)) {
@@ -73,6 +71,37 @@ export function toWebRequest(req: PikkuHTTPRequest, baseUrl?: string): Request {
73
71
 
74
72
  const SKIP_RESPONSE_HEADERS = new Set(['content-length', 'transfer-encoding'])
75
73
 
74
+ function collectSetCookieHeaders(webResponse: Response): string[] {
75
+ const seen = new Set<string>()
76
+ const values: string[] = []
77
+
78
+ const add = (cookie: string) => {
79
+ if (!cookie || seen.has(cookie)) {
80
+ return
81
+ }
82
+ seen.add(cookie)
83
+ values.push(cookie)
84
+ }
85
+
86
+ const headersWithGetSetCookie = webResponse.headers as Headers & {
87
+ getSetCookie?: () => string[]
88
+ }
89
+
90
+ if (typeof headersWithGetSetCookie.getSetCookie === 'function') {
91
+ for (const cookie of headersWithGetSetCookie.getSetCookie()) {
92
+ add(cookie)
93
+ }
94
+ } else {
95
+ webResponse.headers.forEach((value, name) => {
96
+ if (name.toLowerCase() === 'set-cookie') {
97
+ add(value)
98
+ }
99
+ })
100
+ }
101
+
102
+ return values
103
+ }
104
+
76
105
  /**
77
106
  * Applies a Web API Response to a PikkuHTTPResponse.
78
107
  * Copies status, headers (including Set-Cookie), redirects, and body.
@@ -83,11 +112,16 @@ export async function applyWebResponse(
83
112
  ): Promise<void> {
84
113
  res.status(webResponse.status)
85
114
 
115
+ const setCookieValues = collectSetCookieHeaders(webResponse)
116
+
86
117
  webResponse.headers.forEach((value, name) => {
87
118
  const lower = name.toLowerCase()
88
119
  if (SKIP_RESPONSE_HEADERS.has(lower)) {
89
120
  return
90
121
  }
122
+ if (lower === 'set-cookie') {
123
+ return
124
+ }
91
125
  if (lower === 'location') {
92
126
  res.redirect(value, webResponse.status)
93
127
  } else {
@@ -95,6 +129,10 @@ export async function applyWebResponse(
95
129
  }
96
130
  })
97
131
 
132
+ if (setCookieValues.length > 0) {
133
+ res.header('Set-Cookie', setCookieValues)
134
+ }
135
+
98
136
  const body = await webResponse.text()
99
137
  if (body) {
100
138
  if (res.send) {
@@ -0,0 +1,367 @@
1
+ import { beforeEach, describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ MCPError,
6
+ runMCPPrompt,
7
+ runMCPResource,
8
+ runMCPTool,
9
+ wireMCPPrompt,
10
+ wireMCPResource,
11
+ } from './mcp-runner.js'
12
+ import { addFunction } from '../../function/function-runner.js'
13
+ import { pikkuState, resetPikkuState } from '../../pikku-state.js'
14
+
15
+ const logger = {
16
+ debug: () => {},
17
+ info: () => {},
18
+ warn: () => {},
19
+ error: () => {},
20
+ }
21
+
22
+ const mcpWire = {
23
+ sendResourceUpdated: () => {},
24
+ enableResources: async () => true,
25
+ enablePrompts: async () => true,
26
+ enableTools: async () => true,
27
+ }
28
+
29
+ const registerFunction = (
30
+ funcName: string,
31
+ func: (services: any, data: any, wire: any) => Promise<unknown> | unknown,
32
+ packageName: string | null = null
33
+ ) => {
34
+ addFunction(funcName, { func } as never, packageName)
35
+ pikkuState(packageName, 'function', 'meta')[funcName] = {
36
+ name: funcName,
37
+ sessionless: true,
38
+ permissions: [],
39
+ } as never
40
+ }
41
+
42
+ beforeEach(() => {
43
+ resetPikkuState()
44
+ pikkuState(null, 'package', 'singletonServices', {
45
+ logger,
46
+ } as never)
47
+ })
48
+
49
+ describe('wireMCPResource', () => {
50
+ test('skips registration when metadata is missing', () => {
51
+ const warnings: string[] = []
52
+ const originalWarn = console.warn
53
+ console.warn = (message: string) => {
54
+ warnings.push(message)
55
+ }
56
+
57
+ try {
58
+ wireMCPResource({
59
+ uri: 'resource://missing',
60
+ title: 'Missing',
61
+ description: 'Missing',
62
+ func: { func: async () => ({ ok: true }) } as never,
63
+ })
64
+ } finally {
65
+ console.warn = originalWarn
66
+ }
67
+
68
+ assert.equal(getResourceMap().has('resource://missing'), false)
69
+ assert.match(
70
+ warnings[0] || '',
71
+ /Skipping MCP resource 'resource:\/\/missing'/
72
+ )
73
+ })
74
+
75
+ test('throws when resource is registered twice', () => {
76
+ pikkuState(null, 'mcp', 'resourcesMeta')['resource://users'] = {
77
+ uri: 'resource://users',
78
+ title: 'Users',
79
+ description: 'Users',
80
+ pikkuFuncId: 'resourceFunc',
81
+ inputSchema: null,
82
+ outputSchema: null,
83
+ } as never
84
+ registerFunction('resourceFunc', async () => ({ ok: true }))
85
+
86
+ const resource = {
87
+ uri: 'resource://users',
88
+ title: 'Users',
89
+ description: 'Users',
90
+ func: { func: async () => ({ ok: true }) } as never,
91
+ }
92
+
93
+ wireMCPResource(resource)
94
+
95
+ assert.throws(() => wireMCPResource(resource), {
96
+ message: 'MCP resource already exists: resource://users',
97
+ })
98
+ })
99
+ })
100
+
101
+ describe('wireMCPPrompt', () => {
102
+ test('skips registration when metadata is missing', () => {
103
+ const warnings: string[] = []
104
+ const originalWarn = console.warn
105
+ console.warn = (message: string) => {
106
+ warnings.push(message)
107
+ }
108
+
109
+ try {
110
+ wireMCPPrompt({
111
+ name: 'missing-prompt',
112
+ description: 'Missing',
113
+ func: { func: async () => [] } as never,
114
+ })
115
+ } finally {
116
+ console.warn = originalWarn
117
+ }
118
+
119
+ assert.equal(
120
+ pikkuState(null, 'mcp', 'prompts').has('missing-prompt'),
121
+ false
122
+ )
123
+ assert.match(warnings[0] || '', /Skipping MCP prompt 'missing-prompt'/)
124
+ })
125
+ })
126
+
127
+ describe('runMCPResource', () => {
128
+ test('matches uri templates and merges extracted params into the request', async () => {
129
+ pikkuState(null, 'mcp', 'resourcesMeta')['resource://todos/{id}'] = {
130
+ uri: 'resource://todos/{id}',
131
+ title: 'Todo',
132
+ description: 'Todo',
133
+ pikkuFuncId: 'resourceFunc',
134
+ inputSchema: null,
135
+ outputSchema: null,
136
+ } as never
137
+ registerFunction('resourceFunc', async () => ({ ok: false }))
138
+
139
+ wireMCPResource({
140
+ uri: 'resource://todos/{id}',
141
+ title: 'Todo',
142
+ description: 'Todo',
143
+ func: {
144
+ func: async (_services: any, data: any, wire: any) => ({
145
+ data,
146
+ uri: wire.mcp?.uri,
147
+ }),
148
+ } as never,
149
+ })
150
+
151
+ const response = await runMCPResource(
152
+ {
153
+ jsonrpc: '2.0',
154
+ id: 'req-1',
155
+ params: { include: 'details' },
156
+ },
157
+ { mcp: mcpWire as never },
158
+ 'resource://todos/42'
159
+ )
160
+
161
+ assert.deepEqual(response, {
162
+ id: 'req-1',
163
+ result: {
164
+ data: { include: 'details', id: '42' },
165
+ uri: 'resource://todos/42',
166
+ },
167
+ })
168
+ })
169
+ })
170
+
171
+ describe('runMCPTool', () => {
172
+ test('wraps non-MCPToolResponse output as text', async () => {
173
+ pikkuState(null, 'mcp', 'toolsMeta').echo = {
174
+ name: 'echo',
175
+ title: 'Echo',
176
+ description: 'Echo',
177
+ pikkuFuncId: 'toolFunc',
178
+ inputSchema: null,
179
+ outputSchema: null,
180
+ } as never
181
+ registerFunction('toolFunc', async (_services, data) => data)
182
+
183
+ const response = await runMCPTool(
184
+ {
185
+ jsonrpc: '2.0',
186
+ id: 'req-2',
187
+ params: { hello: 'world' },
188
+ },
189
+ { mcp: mcpWire as never },
190
+ 'echo'
191
+ )
192
+
193
+ assert.deepEqual(response, {
194
+ id: 'req-2',
195
+ result: [{ type: 'text', text: '{"hello":"world"}' }],
196
+ })
197
+ })
198
+
199
+ test('preserves MCPToolResponse output without wrapping', async () => {
200
+ pikkuState(null, 'mcp', 'toolsMeta').toolResponse = {
201
+ name: 'toolResponse',
202
+ title: 'Tool Response',
203
+ description: 'Tool Response',
204
+ pikkuFuncId: 'toolResponseFunc',
205
+ inputSchema: null,
206
+ outputSchema: 'MCPToolResponse',
207
+ } as never
208
+ registerFunction('toolResponseFunc', async () => [
209
+ { type: 'text', text: 'hello' },
210
+ ])
211
+
212
+ const response = await runMCPTool(
213
+ {
214
+ jsonrpc: '2.0',
215
+ id: 'req-3',
216
+ params: {},
217
+ },
218
+ { mcp: mcpWire as never },
219
+ 'toolResponse'
220
+ )
221
+
222
+ assert.deepEqual(response, {
223
+ id: 'req-3',
224
+ result: [{ type: 'text', text: 'hello' }],
225
+ })
226
+ })
227
+
228
+ test('resolves namespaced tool function ids through addon packages', async () => {
229
+ pikkuState(null, 'addons', 'packages').set('addon', {
230
+ package: '@addon/pkg',
231
+ } as never)
232
+ pikkuState(null, 'mcp', 'toolsMeta').addonTool = {
233
+ name: 'addonTool',
234
+ title: 'Addon Tool',
235
+ description: 'Addon Tool',
236
+ pikkuFuncId: 'addon:toolFunc',
237
+ inputSchema: null,
238
+ outputSchema: null,
239
+ } as never
240
+ registerFunction(
241
+ 'toolFunc',
242
+ async () => ({
243
+ packageName: 'root',
244
+ }),
245
+ null
246
+ )
247
+ registerFunction(
248
+ 'toolFunc',
249
+ async (_services, data) => ({
250
+ data,
251
+ packageName: '@addon/pkg',
252
+ }),
253
+ '@addon/pkg'
254
+ )
255
+
256
+ const response = await runMCPTool(
257
+ {
258
+ jsonrpc: '2.0',
259
+ id: 'req-4',
260
+ params: { ok: true },
261
+ },
262
+ { mcp: mcpWire as never },
263
+ 'addonTool'
264
+ )
265
+
266
+ assert.deepEqual(response, {
267
+ id: 'req-4',
268
+ result: [
269
+ {
270
+ type: 'text',
271
+ text: '{"data":{"ok":true},"packageName":"@addon/pkg"}',
272
+ },
273
+ ],
274
+ })
275
+ })
276
+
277
+ test('maps bad jsonrpc version to MCP error code', async () => {
278
+ pikkuState(null, 'mcp', 'toolsMeta').echo = {
279
+ name: 'echo',
280
+ title: 'Echo',
281
+ description: 'Echo',
282
+ pikkuFuncId: 'toolFunc',
283
+ inputSchema: null,
284
+ outputSchema: null,
285
+ } as never
286
+ registerFunction('toolFunc', async () => ({ ok: true }))
287
+
288
+ await assert.rejects(
289
+ () =>
290
+ runMCPTool(
291
+ { jsonrpc: '1.0', id: 'bad-version', params: {} },
292
+ { mcp: mcpWire as never },
293
+ 'echo'
294
+ ),
295
+ (error: unknown) => {
296
+ assert.ok(error instanceof MCPError)
297
+ assert.equal(error.error.code, -32600)
298
+ assert.equal(error.error.id, 'bad-version')
299
+ return true
300
+ }
301
+ )
302
+ })
303
+
304
+ test('maps unmapped runtime errors to internal MCP errors', async () => {
305
+ pikkuState(null, 'mcp', 'toolsMeta').boom = {
306
+ name: 'boom',
307
+ title: 'Boom',
308
+ description: 'Boom',
309
+ pikkuFuncId: 'boomFunc',
310
+ inputSchema: null,
311
+ outputSchema: null,
312
+ } as never
313
+ registerFunction('boomFunc', async () => {
314
+ throw new Error('boom')
315
+ })
316
+
317
+ await assert.rejects(
318
+ () =>
319
+ runMCPTool(
320
+ { jsonrpc: '2.0', id: 'boom-1', params: {} },
321
+ { mcp: mcpWire as never },
322
+ 'boom'
323
+ ),
324
+ (error: unknown) => {
325
+ assert.ok(error instanceof MCPError)
326
+ assert.equal(error.error.code, -32603)
327
+ assert.equal(error.error.data?.message, 'boom')
328
+ return true
329
+ }
330
+ )
331
+ })
332
+ })
333
+
334
+ describe('runMCPPrompt', () => {
335
+ test('maps missing prompt registration to not found MCP errors', async () => {
336
+ pikkuState(null, 'mcp', 'promptsMeta').missingPrompt = {
337
+ name: 'missingPrompt',
338
+ description: 'Missing Prompt',
339
+ pikkuFuncId: 'missingPromptFunc',
340
+ inputSchema: null,
341
+ outputSchema: null,
342
+ arguments: [],
343
+ } as never
344
+
345
+ await assert.rejects(
346
+ () =>
347
+ runMCPPrompt(
348
+ { jsonrpc: '2.0', id: 'prompt-1', params: {} },
349
+ { mcp: mcpWire as never },
350
+ 'missingPrompt'
351
+ ),
352
+ (error: unknown) => {
353
+ assert.ok(error instanceof MCPError)
354
+ assert.equal(error.error.code, -32601)
355
+ assert.match(
356
+ error.error.message,
357
+ /server cannot find the requested resource/i
358
+ )
359
+ return true
360
+ }
361
+ )
362
+ })
363
+ })
364
+
365
+ const getResourceMap = () => {
366
+ return pikkuState(null, 'mcp', 'resources')
367
+ }
@@ -161,11 +161,8 @@ export type QueueWorkersMeta = Record<
161
161
  * Core queue processor definition
162
162
  */
163
163
  export type CoreQueueWorker<
164
- PikkuFunctionConfig extends CorePikkuFunctionConfig<
165
- any,
166
- any,
167
- any
168
- > = CorePikkuFunctionConfig<any, any, any>,
164
+ PikkuFunctionConfig extends CorePikkuFunctionConfig<any, any, any> =
165
+ CorePikkuFunctionConfig<any, any, any>,
169
166
  > = {
170
167
  name: string
171
168
  func: PikkuFunctionConfig