@pikku/core 0.12.20 → 0.12.21
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/CHANGELOG.md +11 -0
- package/dist/dev/hot-reload.js +20 -6
- package/dist/errors/error-handler.d.ts +1 -1
- package/dist/errors/error-handler.js +2 -2
- package/dist/function/functions.types.d.ts +3 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/middleware/index.d.ts +2 -2
- package/dist/middleware/index.js +2 -2
- package/dist/middleware/telemetry.d.ts +2 -2
- package/dist/middleware/telemetry.js +2 -2
- package/dist/middleware-runner.d.ts +15 -27
- package/dist/middleware-runner.js +25 -30
- package/dist/permissions.d.ts +15 -22
- package/dist/permissions.js +30 -25
- package/dist/pikku-request.js +1 -1
- package/dist/pikku-state.js +2 -3
- package/dist/services/ai-agent-runner-service.d.ts +1 -0
- package/dist/services/in-memory-queue-service.js +1 -1
- package/dist/services/in-memory-workflow-service.d.ts +15 -13
- package/dist/services/in-memory-workflow-service.js +31 -13
- package/dist/services/local-content.js +8 -1
- package/dist/services/user-session-service.d.ts +1 -1
- package/dist/testing/service-tests.js +24 -0
- package/dist/types/core.types.d.ts +4 -0
- package/dist/types/state.types.d.ts +2 -18
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
- package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
- package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
- package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
- package/dist/wirings/channel/channel-handler.js +1 -1
- package/dist/wirings/channel/channel-store.d.ts +13 -0
- package/dist/wirings/channel/channel.types.d.ts +3 -0
- package/dist/wirings/channel/local/local-channel-runner.js +23 -5
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
- package/dist/wirings/cli/cli-runner.js +24 -5
- package/dist/wirings/cli/command-parser.js +19 -4
- package/dist/wirings/http/http-runner.js +72 -36
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
- package/dist/wirings/http/web-request.js +32 -0
- package/dist/wirings/rpc/rpc-runner.js +13 -3
- package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
- package/dist/wirings/workflow/graph/graph-node.js +4 -0
- package/dist/wirings/workflow/graph/graph-runner.js +12 -10
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
- package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
- package/dist/wirings/workflow/workflow.types.d.ts +34 -0
- package/package.json +1 -1
- package/run-tests.sh +4 -1
- package/src/dev/hot-reload.test.ts +62 -11
- package/src/dev/hot-reload.ts +28 -7
- package/src/errors/error-handler.ts +8 -2
- package/src/errors/error.test.ts +2 -0
- package/src/function/function-runner.test.ts +500 -10
- package/src/function/functions.types.ts +3 -2
- package/src/index.ts +12 -2
- package/src/middleware/index.ts +11 -2
- package/src/middleware/telemetry.ts +2 -2
- package/src/middleware-runner.test.ts +16 -16
- package/src/middleware-runner.ts +42 -30
- package/src/permissions.test.ts +27 -24
- package/src/permissions.ts +41 -25
- package/src/pikku-request.test.ts +35 -0
- package/src/pikku-request.ts +1 -1
- package/src/pikku-state.ts +2 -3
- package/src/run-tests-script.test.ts +18 -0
- package/src/services/ai-agent-runner-service.ts +1 -0
- package/src/services/in-memory-queue-service.ts +1 -1
- package/src/services/in-memory-session-store.ts +1 -2
- package/src/services/in-memory-workflow-service.test.ts +33 -11
- package/src/services/in-memory-workflow-service.ts +49 -13
- package/src/services/local-content.test.ts +54 -0
- package/src/services/local-content.ts +12 -1
- package/src/services/typed-credential-service.ts +3 -3
- package/src/services/typed-secret-service.ts +3 -3
- package/src/services/typed-variables-service.ts +3 -3
- package/src/services/user-session-service.ts +4 -4
- package/src/testing/service-tests.ts +30 -0
- package/src/types/core.types.ts +6 -2
- package/src/types/state.types.ts +2 -13
- package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
- package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
- package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
- package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
- package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
- package/src/wirings/channel/channel-handler.test.ts +272 -0
- package/src/wirings/channel/channel-handler.ts +1 -1
- package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
- package/src/wirings/channel/channel-store.ts +19 -0
- package/src/wirings/channel/channel.types.ts +4 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
- package/src/wirings/channel/local/local-channel-runner.ts +41 -5
- package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
- package/src/wirings/cli/cli-runner.test.ts +255 -2
- package/src/wirings/cli/cli-runner.ts +31 -10
- package/src/wirings/cli/cli.types.ts +4 -8
- package/src/wirings/cli/command-parser.test.ts +83 -0
- package/src/wirings/cli/command-parser.ts +23 -4
- package/src/wirings/http/http-runner.test.ts +296 -1
- package/src/wirings/http/http-runner.ts +87 -57
- package/src/wirings/http/http.types.ts +11 -26
- package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
- package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
- package/src/wirings/http/web-request.test.ts +115 -0
- package/src/wirings/http/web-request.ts +43 -5
- package/src/wirings/mcp/mcp-runner.test.ts +367 -0
- package/src/wirings/queue/queue.types.ts +2 -5
- package/src/wirings/rpc/rpc-runner.test.ts +511 -0
- package/src/wirings/rpc/rpc-runner.ts +15 -3
- package/src/wirings/rpc/wire-addon.test.ts +57 -0
- package/src/wirings/workflow/graph/graph-node.ts +12 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
- package/src/wirings/workflow/graph/graph-runner.ts +28 -10
- package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
- package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
- package/src/wirings/workflow/workflow.types.ts +68 -5
- 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 =
|
|
11
|
-
|
|
12
|
-
|
|
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
|