@pikku/core 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/CHANGELOG.md +21 -1
- package/dist/dev/hot-reload.d.ts +10 -0
- package/dist/dev/hot-reload.js +158 -0
- package/dist/middleware-runner.d.ts +1 -0
- package/dist/middleware-runner.js +5 -0
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.js +5 -0
- package/dist/services/content-service.d.ts +2 -0
- package/dist/services/in-memory-workflow-service.d.ts +1 -0
- package/dist/services/in-memory-workflow-service.js +16 -11
- package/dist/services/workflow-service.d.ts +1 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.js +16 -1
- package/dist/wirings/channel/channel-middleware-runner.d.ts +1 -0
- package/dist/wirings/channel/channel-middleware-runner.js +5 -0
- package/dist/wirings/http/http-runner.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +6 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +51 -16
- package/dist/wirings/workflow/workflow.types.d.ts +2 -0
- package/package.json +5 -3
- package/src/crypto-utils.test.ts +214 -0
- package/src/crypto-utils.ts +213 -0
- package/src/dev/hot-reload.test.ts +484 -0
- package/src/dev/hot-reload.ts +212 -0
- package/src/errors/error-handler.ts +62 -0
- package/src/errors/error.test.ts +195 -0
- package/src/errors/errors.ts +374 -0
- package/src/errors/index.ts +2 -0
- package/src/factory-functions.test.ts +109 -0
- package/src/function/function-runner.test.ts +536 -0
- package/src/function/function-runner.ts +365 -0
- package/src/function/functions.types.ts +304 -0
- package/src/function/index.ts +5 -0
- package/src/handle-error.test.ts +424 -0
- package/src/handle-error.ts +69 -0
- package/src/index.ts +118 -0
- package/src/internal.ts +6 -0
- package/src/middleware/auth-apikey.test.ts +363 -0
- package/src/middleware/auth-apikey.ts +47 -0
- package/src/middleware/auth-bearer.test.ts +450 -0
- package/src/middleware/auth-bearer.ts +72 -0
- package/src/middleware/auth-cookie.test.ts +528 -0
- package/src/middleware/auth-cookie.ts +80 -0
- package/src/middleware/cors.test.ts +424 -0
- package/src/middleware/cors.ts +104 -0
- package/src/middleware/index.ts +5 -0
- package/src/middleware/remote-auth.test.ts +488 -0
- package/src/middleware/remote-auth.ts +68 -0
- package/src/middleware/timeout.ts +15 -0
- package/src/middleware-runner.test.ts +418 -0
- package/src/middleware-runner.ts +240 -0
- package/src/permissions.test.ts +434 -0
- package/src/permissions.ts +327 -0
- package/src/pikku-request.ts +23 -0
- package/src/pikku-response.ts +5 -0
- package/src/pikku-state.test.ts +224 -0
- package/src/pikku-state.ts +216 -0
- package/src/run-tests-script.test.ts +49 -0
- package/src/schema.test.ts +249 -0
- package/src/schema.ts +151 -0
- package/src/services/ai-agent-runner-service.ts +41 -0
- package/src/services/ai-run-state-service.ts +20 -0
- package/src/services/ai-storage-service.ts +39 -0
- package/src/services/content-service.ts +81 -0
- package/src/services/deployment-service.ts +22 -0
- package/src/services/gateway-service.ts +20 -0
- package/src/services/gopass-secrets.ts +98 -0
- package/src/services/in-memory-ai-run-state-service.ts +73 -0
- package/src/services/in-memory-trigger-service.ts +64 -0
- package/src/services/in-memory-workflow-service.test.ts +351 -0
- package/src/services/in-memory-workflow-service.ts +512 -0
- package/src/services/index.ts +56 -0
- package/src/services/jwt-service.ts +30 -0
- package/src/services/local-content.ts +120 -0
- package/src/services/local-gateway-service.ts +62 -0
- package/src/services/local-secrets.test.ts +80 -0
- package/src/services/local-secrets.ts +94 -0
- package/src/services/local-variables.test.ts +61 -0
- package/src/services/local-variables.ts +39 -0
- package/src/services/logger-console.test.ts +118 -0
- package/src/services/logger-console.ts +98 -0
- package/src/services/logger.ts +57 -0
- package/src/services/scheduler-service.ts +86 -0
- package/src/services/schema-service.ts +33 -0
- package/src/services/scoped-secret-service.test.ts +74 -0
- package/src/services/scoped-secret-service.ts +41 -0
- package/src/services/secret-service.ts +38 -0
- package/src/services/trigger-service.ts +17 -0
- package/src/services/typed-secret-service.test.ts +93 -0
- package/src/services/typed-secret-service.ts +70 -0
- package/src/services/typed-variables-service.test.ts +73 -0
- package/src/services/typed-variables-service.ts +81 -0
- package/src/services/user-session-service.test.ts +113 -0
- package/src/services/user-session-service.ts +86 -0
- package/src/services/variables-service.ts +11 -0
- package/src/services/workflow-service.ts +96 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/service-tests.ts +873 -0
- package/src/time-utils.test.ts +56 -0
- package/src/time-utils.ts +107 -0
- package/src/types/core.types.ts +478 -0
- package/src/types/state.types.ts +188 -0
- package/src/utils/hash.test.ts +68 -0
- package/src/utils/hash.ts +26 -0
- package/src/utils.test.ts +350 -0
- package/src/utils.ts +129 -0
- package/src/version.test.ts +80 -0
- package/src/version.ts +25 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
- package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
- package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +310 -0
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
- package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +659 -0
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
- package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +319 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +773 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +859 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +1153 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +382 -0
- package/src/wirings/ai-agent/index.ts +37 -0
- package/src/wirings/channel/channel-common.ts +90 -0
- package/src/wirings/channel/channel-handler.ts +250 -0
- package/src/wirings/channel/channel-middleware-runner.ts +126 -0
- package/src/wirings/channel/channel-runner.ts +166 -0
- package/src/wirings/channel/channel-store.ts +29 -0
- package/src/wirings/channel/channel.types.ts +172 -0
- package/src/wirings/channel/define-channel-routes.ts +25 -0
- package/src/wirings/channel/eventhub-service.ts +34 -0
- package/src/wirings/channel/eventhub-store.ts +13 -0
- package/src/wirings/channel/index.ts +24 -0
- package/src/wirings/channel/local/index.ts +3 -0
- package/src/wirings/channel/local/local-channel-handler.ts +81 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +215 -0
- package/src/wirings/channel/local/local-channel-runner.ts +210 -0
- package/src/wirings/channel/local/local-eventhub-service.test.ts +95 -0
- package/src/wirings/channel/local/local-eventhub-service.ts +101 -0
- package/src/wirings/channel/log-channels.ts +20 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +54 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +45 -0
- package/src/wirings/channel/serverless/index.ts +5 -0
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +289 -0
- package/src/wirings/cli/channel/cli-channel-runner.ts +136 -0
- package/src/wirings/cli/channel/index.ts +1 -0
- package/src/wirings/cli/cli-runner.test.ts +403 -0
- package/src/wirings/cli/cli-runner.ts +529 -0
- package/src/wirings/cli/cli.types.ts +358 -0
- package/src/wirings/cli/command-parser.test.ts +445 -0
- package/src/wirings/cli/command-parser.ts +504 -0
- package/src/wirings/cli/define-cli-commands.ts +24 -0
- package/src/wirings/cli/index.ts +19 -0
- package/src/wirings/gateway/gateway-runner.test.ts +474 -0
- package/src/wirings/gateway/gateway-runner.ts +411 -0
- package/src/wirings/gateway/gateway.types.ts +149 -0
- package/src/wirings/gateway/index.ts +13 -0
- package/src/wirings/http/http-routes.test.ts +322 -0
- package/src/wirings/http/http-routes.ts +197 -0
- package/src/wirings/http/http-runner.test.ts +144 -0
- package/src/wirings/http/http-runner.ts +588 -0
- package/src/wirings/http/http.types.ts +369 -0
- package/src/wirings/http/index.ts +28 -0
- package/src/wirings/http/log-http-routes.ts +22 -0
- package/src/wirings/http/pikku-fetch-http-request.test.ts +237 -0
- package/src/wirings/http/pikku-fetch-http-request.ts +170 -0
- package/src/wirings/http/pikku-fetch-http-response.test.ts +82 -0
- package/src/wirings/http/pikku-fetch-http-response.ts +147 -0
- package/src/wirings/http/routers/http-router.ts +13 -0
- package/src/wirings/http/routers/path-to-regex.test.ts +319 -0
- package/src/wirings/http/routers/path-to-regex.ts +126 -0
- package/src/wirings/http/web-request.test.ts +236 -0
- package/src/wirings/http/web-request.ts +104 -0
- package/src/wirings/mcp/index.ts +27 -0
- package/src/wirings/mcp/mcp-endpoint-registry.test.ts +389 -0
- package/src/wirings/mcp/mcp-endpoint-registry.ts +159 -0
- package/src/wirings/mcp/mcp-runner.ts +323 -0
- package/src/wirings/mcp/mcp.types.ts +216 -0
- package/src/wirings/node/index.ts +2 -0
- package/src/wirings/node/node.types.ts +23 -0
- package/src/wirings/oauth2/index.ts +3 -0
- package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
- package/src/wirings/oauth2/oauth2-client.ts +335 -0
- package/src/wirings/oauth2/oauth2.types.ts +69 -0
- package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
- package/src/wirings/queue/index.ts +31 -0
- package/src/wirings/queue/queue-runner.test.ts +710 -0
- package/src/wirings/queue/queue-runner.ts +192 -0
- package/src/wirings/queue/queue.types.ts +189 -0
- package/src/wirings/queue/register-queue-helper.ts +60 -0
- package/src/wirings/queue/validate-worker-config.test.ts +108 -0
- package/src/wirings/queue/validate-worker-config.ts +116 -0
- package/src/wirings/rpc/index.ts +4 -0
- package/src/wirings/rpc/rpc-runner.ts +460 -0
- package/src/wirings/rpc/rpc-types.ts +56 -0
- package/src/wirings/rpc/wire-addon.ts +21 -0
- package/src/wirings/scheduler/index.ts +11 -0
- package/src/wirings/scheduler/log-schedulers.ts +20 -0
- package/src/wirings/scheduler/scheduler-runner.test.ts +660 -0
- package/src/wirings/scheduler/scheduler-runner.ts +133 -0
- package/src/wirings/scheduler/scheduler.types.ts +53 -0
- package/src/wirings/secret/index.ts +9 -0
- package/src/wirings/secret/secret.types.ts +32 -0
- package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
- package/src/wirings/secret/validate-secret-definitions.ts +82 -0
- package/src/wirings/trigger/index.ts +10 -0
- package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
- package/src/wirings/trigger/trigger-runner.test.ts +79 -0
- package/src/wirings/trigger/trigger-runner.ts +135 -0
- package/src/wirings/trigger/trigger.types.ts +178 -0
- package/src/wirings/variable/index.ts +8 -0
- package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
- package/src/wirings/variable/validate-variable-definitions.ts +69 -0
- package/src/wirings/variable/variable.types.ts +22 -0
- package/src/wirings/workflow/dsl/index.ts +31 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +320 -0
- package/src/wirings/workflow/dsl/workflow-runner.ts +28 -0
- package/src/wirings/workflow/graph/graph-node.ts +222 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
- package/src/wirings/workflow/graph/graph-runner.ts +816 -0
- package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
- package/src/wirings/workflow/graph/graph-validation.ts +237 -0
- package/src/wirings/workflow/graph/index.ts +19 -0
- package/src/wirings/workflow/graph/template.test.ts +49 -0
- package/src/wirings/workflow/graph/template.ts +42 -0
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +104 -0
- package/src/wirings/workflow/index.ts +82 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +1229 -0
- package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
- package/src/wirings/workflow/workflow-helpers.ts +79 -0
- package/src/wirings/workflow/workflow.types.ts +276 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/lcov.info +0 -18891
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { PikkuHTTPRequest } from './http.types.js'
|
|
2
|
+
import type { PikkuHTTPResponse } from './http.types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts a PikkuHTTPRequest into a Web API Request.
|
|
6
|
+
* Useful for bridging Pikku routes to libraries that expect standard Web Request objects.
|
|
7
|
+
*/
|
|
8
|
+
export function toWebRequest(req: PikkuHTTPRequest, baseUrl?: string): Request {
|
|
9
|
+
const url = new URL(
|
|
10
|
+
req.path(),
|
|
11
|
+
baseUrl ?? `http://${req.header('host') ?? 'localhost'}`
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const query = req.query()
|
|
15
|
+
for (const [key, value] of Object.entries(query)) {
|
|
16
|
+
if (value != null) {
|
|
17
|
+
url.searchParams.set(key, String(value))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const headers = new Headers(req.headers())
|
|
22
|
+
|
|
23
|
+
const method = req.method().toUpperCase()
|
|
24
|
+
const hasBody = !['GET', 'HEAD', 'OPTIONS'].includes(method)
|
|
25
|
+
|
|
26
|
+
if (!hasBody) {
|
|
27
|
+
return new Request(url, { method, headers })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return new Request(url, {
|
|
31
|
+
method,
|
|
32
|
+
headers,
|
|
33
|
+
body: new ReadableStream({
|
|
34
|
+
async start(controller) {
|
|
35
|
+
try {
|
|
36
|
+
const buffer = await req.arrayBuffer()
|
|
37
|
+
if (buffer.byteLength > 0) {
|
|
38
|
+
controller.enqueue(new Uint8Array(buffer))
|
|
39
|
+
} else {
|
|
40
|
+
// arrayBuffer may be empty if body was pre-parsed (e.g., Express middleware)
|
|
41
|
+
const contentType = (
|
|
42
|
+
headers.get('content-type') || ''
|
|
43
|
+
).toLowerCase()
|
|
44
|
+
const parsed = await req.json()
|
|
45
|
+
if (
|
|
46
|
+
parsed &&
|
|
47
|
+
typeof parsed === 'object' &&
|
|
48
|
+
Object.keys(parsed as any).length > 0
|
|
49
|
+
) {
|
|
50
|
+
let reconstructed: string
|
|
51
|
+
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
52
|
+
reconstructed = new URLSearchParams(
|
|
53
|
+
parsed as Record<string, string>
|
|
54
|
+
).toString()
|
|
55
|
+
} else {
|
|
56
|
+
reconstructed = JSON.stringify(parsed)
|
|
57
|
+
}
|
|
58
|
+
controller.enqueue(new TextEncoder().encode(reconstructed))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Empty body
|
|
63
|
+
}
|
|
64
|
+
controller.close()
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
// @ts-ignore - duplex is needed for streaming body in Node.js
|
|
68
|
+
duplex: 'half',
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const SKIP_RESPONSE_HEADERS = new Set(['content-length', 'transfer-encoding'])
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Applies a Web API Response to a PikkuHTTPResponse.
|
|
76
|
+
* Copies status, headers (including Set-Cookie), redirects, and body.
|
|
77
|
+
*/
|
|
78
|
+
export async function applyWebResponse(
|
|
79
|
+
res: PikkuHTTPResponse,
|
|
80
|
+
webResponse: Response
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
res.status(webResponse.status)
|
|
83
|
+
|
|
84
|
+
webResponse.headers.forEach((value, name) => {
|
|
85
|
+
const lower = name.toLowerCase()
|
|
86
|
+
if (SKIP_RESPONSE_HEADERS.has(lower)) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
if (lower === 'location') {
|
|
90
|
+
res.redirect(value, webResponse.status)
|
|
91
|
+
} else {
|
|
92
|
+
res.header(name, value)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const body = await webResponse.text()
|
|
97
|
+
if (body) {
|
|
98
|
+
if (res.send) {
|
|
99
|
+
res.send(body)
|
|
100
|
+
} else {
|
|
101
|
+
res.arrayBuffer(body)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { MCPEndpointRegistry } from './mcp-endpoint-registry.js'
|
|
2
|
+
export {
|
|
3
|
+
MCPError,
|
|
4
|
+
wireMCPResource,
|
|
5
|
+
wireMCPPrompt,
|
|
6
|
+
runMCPResource,
|
|
7
|
+
runMCPTool,
|
|
8
|
+
runMCPPrompt,
|
|
9
|
+
} from './mcp-runner.js'
|
|
10
|
+
export {
|
|
11
|
+
getMCPResourcesMeta,
|
|
12
|
+
getMCPToolsMeta,
|
|
13
|
+
getMCPPromptsMeta,
|
|
14
|
+
} from './mcp-runner.js'
|
|
15
|
+
export type {
|
|
16
|
+
AssertMCPResourceURIParams,
|
|
17
|
+
CoreMCPPrompt,
|
|
18
|
+
CoreMCPResource,
|
|
19
|
+
CoreMCPTool,
|
|
20
|
+
MCPPromptResponse,
|
|
21
|
+
MCPResourceMeta,
|
|
22
|
+
MCPResourceResponse,
|
|
23
|
+
MCPToolMeta,
|
|
24
|
+
MCPToolResponse,
|
|
25
|
+
MCPPromptMeta,
|
|
26
|
+
PikkuMCP,
|
|
27
|
+
} from './mcp.types.js'
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { test, describe, beforeEach } from 'node:test'
|
|
2
|
+
import * as assert from 'assert'
|
|
3
|
+
import { MCPEndpointRegistry } from './mcp-endpoint-registry.js'
|
|
4
|
+
|
|
5
|
+
describe('MCPEndpointRegistry', () => {
|
|
6
|
+
let registry: MCPEndpointRegistry
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
registry = new MCPEndpointRegistry()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('constructor', () => {
|
|
13
|
+
test('should initialize with empty maps', () => {
|
|
14
|
+
assert.deepStrictEqual(registry.getTools(), [])
|
|
15
|
+
assert.deepStrictEqual(registry.getResources(), [])
|
|
16
|
+
assert.deepStrictEqual(registry.getPrompts(), [])
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('loadFromMCPJson', () => {
|
|
21
|
+
test('should load tools from MCP JSON', async () => {
|
|
22
|
+
const mockData = {
|
|
23
|
+
tools: [
|
|
24
|
+
{
|
|
25
|
+
name: 'test-tool',
|
|
26
|
+
title: 'Test Tool',
|
|
27
|
+
description: 'A test tool',
|
|
28
|
+
parameters: { type: 'object' },
|
|
29
|
+
returns: { type: 'string' },
|
|
30
|
+
enabled: true,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await registry.loadFromMCPJson(mockData)
|
|
36
|
+
|
|
37
|
+
const tools = registry.getTools()
|
|
38
|
+
assert.strictEqual(tools.length, 1)
|
|
39
|
+
assert.strictEqual(tools[0].name, 'test-tool')
|
|
40
|
+
assert.strictEqual(tools[0].title, 'Test Tool')
|
|
41
|
+
assert.strictEqual(tools[0].description, 'A test tool')
|
|
42
|
+
assert.deepStrictEqual(tools[0].inputSchema, { type: 'object' })
|
|
43
|
+
assert.deepStrictEqual(tools[0].outputSchema, { type: 'string' })
|
|
44
|
+
assert.strictEqual(tools[0].enabled, true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('should load resources from MCP JSON', async () => {
|
|
48
|
+
const mockData = {
|
|
49
|
+
resources: [
|
|
50
|
+
{
|
|
51
|
+
name: 'test-resource',
|
|
52
|
+
uri: 'file://test.txt',
|
|
53
|
+
description: 'A test resource',
|
|
54
|
+
parameters: { type: 'object' },
|
|
55
|
+
enabled: false,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await registry.loadFromMCPJson(mockData)
|
|
61
|
+
|
|
62
|
+
const resources = registry.getResources()
|
|
63
|
+
assert.strictEqual(resources.length, 1)
|
|
64
|
+
assert.strictEqual(resources[0].title, 'test-resource')
|
|
65
|
+
assert.strictEqual(resources[0].uri, 'file://test.txt')
|
|
66
|
+
assert.strictEqual(resources[0].description, 'A test resource')
|
|
67
|
+
assert.deepStrictEqual(resources[0].inputSchema, { type: 'object' })
|
|
68
|
+
assert.strictEqual(resources[0].enabled, false)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('should load prompts from MCP JSON', async () => {
|
|
72
|
+
const mockData = {
|
|
73
|
+
prompts: [
|
|
74
|
+
{
|
|
75
|
+
name: 'test-prompt',
|
|
76
|
+
description: 'A test prompt',
|
|
77
|
+
arguments: { type: 'object' },
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await registry.loadFromMCPJson(mockData)
|
|
83
|
+
|
|
84
|
+
const prompts = registry.getPrompts()
|
|
85
|
+
assert.strictEqual(prompts.length, 1)
|
|
86
|
+
assert.strictEqual(prompts[0].name, 'test-prompt')
|
|
87
|
+
assert.strictEqual(prompts[0].description, 'A test prompt')
|
|
88
|
+
assert.deepStrictEqual(prompts[0].inputSchema, { type: 'object' })
|
|
89
|
+
assert.strictEqual(prompts[0].enabled, true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('should handle tools with default enabled state', async () => {
|
|
93
|
+
const mockData = {
|
|
94
|
+
tools: [
|
|
95
|
+
{
|
|
96
|
+
name: 'default-tool',
|
|
97
|
+
title: 'Default Tool',
|
|
98
|
+
description: 'Tool with default enabled state',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await registry.loadFromMCPJson(mockData)
|
|
104
|
+
|
|
105
|
+
const tools = registry.getTools()
|
|
106
|
+
assert.strictEqual(tools.length, 1)
|
|
107
|
+
assert.strictEqual(tools[0].enabled, true)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('should handle empty arrays', async () => {
|
|
111
|
+
const mockData = {
|
|
112
|
+
tools: [],
|
|
113
|
+
resources: [],
|
|
114
|
+
prompts: [],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await registry.loadFromMCPJson(mockData)
|
|
118
|
+
|
|
119
|
+
assert.deepStrictEqual(registry.getTools(), [])
|
|
120
|
+
assert.deepStrictEqual(registry.getResources(), [])
|
|
121
|
+
assert.deepStrictEqual(registry.getPrompts(), [])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('should handle missing arrays', async () => {
|
|
125
|
+
const mockData = {}
|
|
126
|
+
|
|
127
|
+
await registry.loadFromMCPJson(mockData)
|
|
128
|
+
|
|
129
|
+
assert.deepStrictEqual(registry.getTools(), [])
|
|
130
|
+
assert.deepStrictEqual(registry.getResources(), [])
|
|
131
|
+
assert.deepStrictEqual(registry.getPrompts(), [])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('should handle empty object gracefully', async () => {
|
|
135
|
+
// Test with empty object
|
|
136
|
+
await registry.loadFromMCPJson({})
|
|
137
|
+
assert.deepStrictEqual(registry.getTools(), [])
|
|
138
|
+
assert.deepStrictEqual(registry.getResources(), [])
|
|
139
|
+
assert.deepStrictEqual(registry.getPrompts(), [])
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('meta setters and getters', () => {
|
|
144
|
+
test('should set and check resource meta', () => {
|
|
145
|
+
const meta = {
|
|
146
|
+
'test-resource': {
|
|
147
|
+
uri: 'file://test.txt',
|
|
148
|
+
title: 'Test Resource',
|
|
149
|
+
description: 'Test description',
|
|
150
|
+
pikkuFuncId: 'testResource',
|
|
151
|
+
inputSchema: null,
|
|
152
|
+
outputSchema: null,
|
|
153
|
+
mimeType: 'text/plain',
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
registry.setResourcesMeta(meta)
|
|
157
|
+
assert.strictEqual(registry.hasResource('test-resource'), true)
|
|
158
|
+
assert.strictEqual(registry.hasResource('nonexistent'), false)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('should set and check tool meta', () => {
|
|
162
|
+
const meta = {
|
|
163
|
+
'test-tool': {
|
|
164
|
+
name: 'test-tool',
|
|
165
|
+
title: 'Test Tool',
|
|
166
|
+
description: 'Test description',
|
|
167
|
+
pikkuFuncId: 'testTool',
|
|
168
|
+
inputSchema: null,
|
|
169
|
+
outputSchema: null,
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
registry.setToolsMeta(meta)
|
|
173
|
+
assert.strictEqual(registry.hasTool('test-tool'), true)
|
|
174
|
+
assert.strictEqual(registry.hasTool('nonexistent'), false)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('should set and check prompt meta', () => {
|
|
178
|
+
const meta = {
|
|
179
|
+
'test-prompt': {
|
|
180
|
+
name: 'test-prompt',
|
|
181
|
+
description: 'Test Prompt',
|
|
182
|
+
pikkuFuncId: 'testPrompt',
|
|
183
|
+
inputSchema: null,
|
|
184
|
+
outputSchema: null,
|
|
185
|
+
arguments: [],
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
registry.setPromptsMeta(meta)
|
|
189
|
+
assert.strictEqual(registry.hasPrompt('test-prompt'), true)
|
|
190
|
+
assert.strictEqual(registry.hasPrompt('nonexistent'), false)
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe('individual item getters', () => {
|
|
195
|
+
beforeEach(async () => {
|
|
196
|
+
const mockData = {
|
|
197
|
+
tools: [
|
|
198
|
+
{
|
|
199
|
+
name: 'tool1',
|
|
200
|
+
title: 'Tool 1',
|
|
201
|
+
description: 'First tool',
|
|
202
|
+
enabled: true,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
resources: [
|
|
206
|
+
{
|
|
207
|
+
name: 'resource1',
|
|
208
|
+
uri: 'file://resource1.txt',
|
|
209
|
+
description: 'First resource',
|
|
210
|
+
enabled: true,
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
prompts: [
|
|
214
|
+
{
|
|
215
|
+
name: 'prompt1',
|
|
216
|
+
description: 'First prompt',
|
|
217
|
+
enabled: true,
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await registry.loadFromMCPJson(mockData)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('should get tool by name', () => {
|
|
226
|
+
const tool = registry.getTool('tool1')
|
|
227
|
+
assert.strictEqual(tool?.name, 'tool1')
|
|
228
|
+
assert.strictEqual(tool?.title, 'Tool 1')
|
|
229
|
+
assert.strictEqual(registry.getTool('nonexistent'), undefined)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('should get resource by name', () => {
|
|
233
|
+
const resource = registry.getResource('resource1')
|
|
234
|
+
assert.strictEqual(resource?.title, 'resource1')
|
|
235
|
+
assert.strictEqual(resource?.uri, 'file://resource1.txt')
|
|
236
|
+
assert.strictEqual(registry.getResource('nonexistent'), undefined)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('should get prompt by name', () => {
|
|
240
|
+
const prompt = registry.getPrompt('prompt1')
|
|
241
|
+
assert.strictEqual(prompt?.name, 'prompt1')
|
|
242
|
+
assert.strictEqual(prompt?.description, 'First prompt')
|
|
243
|
+
assert.strictEqual(registry.getPrompt('nonexistent'), undefined)
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
describe('getTools filters enabled tools', () => {
|
|
248
|
+
test('should only return enabled tools', async () => {
|
|
249
|
+
const mockData = {
|
|
250
|
+
tools: [
|
|
251
|
+
{
|
|
252
|
+
name: 'enabled-tool',
|
|
253
|
+
title: 'Enabled Tool',
|
|
254
|
+
description: 'This tool is enabled',
|
|
255
|
+
enabled: true,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'disabled-tool',
|
|
259
|
+
title: 'Disabled Tool',
|
|
260
|
+
description: 'This tool is disabled',
|
|
261
|
+
enabled: false,
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await registry.loadFromMCPJson(mockData)
|
|
267
|
+
|
|
268
|
+
const tools = registry.getTools()
|
|
269
|
+
assert.strictEqual(tools.length, 1)
|
|
270
|
+
assert.strictEqual(tools[0].name, 'enabled-tool')
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe('enable/disable methods', () => {
|
|
275
|
+
beforeEach(async () => {
|
|
276
|
+
const mockData = {
|
|
277
|
+
tools: [
|
|
278
|
+
{
|
|
279
|
+
name: 'tool1',
|
|
280
|
+
title: 'Tool 1',
|
|
281
|
+
description: 'First tool',
|
|
282
|
+
enabled: true,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'tool2',
|
|
286
|
+
title: 'Tool 2',
|
|
287
|
+
description: 'Second tool',
|
|
288
|
+
enabled: false,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
resources: [
|
|
292
|
+
{
|
|
293
|
+
name: 'resource1',
|
|
294
|
+
uri: 'file://resource1.txt',
|
|
295
|
+
description: 'First resource',
|
|
296
|
+
enabled: true,
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
prompts: [
|
|
300
|
+
{
|
|
301
|
+
name: 'prompt1',
|
|
302
|
+
description: 'First prompt',
|
|
303
|
+
enabled: true,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await registry.loadFromMCPJson(mockData)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('should enable/disable tools and return change status', () => {
|
|
312
|
+
let changed = registry.enableTools({ tool1: false, tool2: true })
|
|
313
|
+
assert.strictEqual(changed, true)
|
|
314
|
+
|
|
315
|
+
const tool1 = registry.getTool('tool1')
|
|
316
|
+
const tool2 = registry.getTool('tool2')
|
|
317
|
+
assert.strictEqual(tool1?.enabled, false)
|
|
318
|
+
assert.strictEqual(tool2?.enabled, true)
|
|
319
|
+
|
|
320
|
+
changed = registry.enableTools({ tool1: false, tool2: true })
|
|
321
|
+
assert.strictEqual(changed, false)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('should handle nonexistent tools', () => {
|
|
325
|
+
const changed = registry.enableTools({ nonexistent: true })
|
|
326
|
+
assert.strictEqual(changed, false)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('should enable/disable prompts and return change status', () => {
|
|
330
|
+
let changed = registry.enablePrompts({ prompt1: false })
|
|
331
|
+
assert.strictEqual(changed, true)
|
|
332
|
+
|
|
333
|
+
const prompt1 = registry.getPrompt('prompt1')
|
|
334
|
+
assert.strictEqual(prompt1?.enabled, false)
|
|
335
|
+
|
|
336
|
+
changed = registry.enablePrompts({ prompt1: false })
|
|
337
|
+
assert.strictEqual(changed, false)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
test('should handle nonexistent prompts', () => {
|
|
341
|
+
const changed = registry.enablePrompts({ nonexistent: true })
|
|
342
|
+
assert.strictEqual(changed, false)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('should enable/disable resources', () => {
|
|
346
|
+
const changed = registry.enableResources({ resource1: false })
|
|
347
|
+
assert.strictEqual(changed, true)
|
|
348
|
+
|
|
349
|
+
const resource1 = registry.getResource('resource1')
|
|
350
|
+
assert.strictEqual(resource1?.enabled, false)
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
describe('edge cases', () => {
|
|
355
|
+
test('should handle null/undefined values in MCP JSON', async () => {
|
|
356
|
+
const mockData = {
|
|
357
|
+
tools: [
|
|
358
|
+
{
|
|
359
|
+
name: 'incomplete-tool',
|
|
360
|
+
title: null,
|
|
361
|
+
description: undefined,
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
await registry.loadFromMCPJson(mockData)
|
|
367
|
+
|
|
368
|
+
const tools = registry.getTools()
|
|
369
|
+
assert.strictEqual(tools.length, 1)
|
|
370
|
+
assert.strictEqual(tools[0].name, 'incomplete-tool')
|
|
371
|
+
assert.strictEqual(tools[0].title, null)
|
|
372
|
+
assert.strictEqual(tools[0].description, undefined)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test('should handle non-array values for tools/resources/prompts', async () => {
|
|
376
|
+
const mockData = {
|
|
377
|
+
tools: 'not an array',
|
|
378
|
+
resources: null,
|
|
379
|
+
prompts: undefined,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await registry.loadFromMCPJson(mockData)
|
|
383
|
+
|
|
384
|
+
assert.deepStrictEqual(registry.getTools(), [])
|
|
385
|
+
assert.deepStrictEqual(registry.getResources(), [])
|
|
386
|
+
assert.deepStrictEqual(registry.getPrompts(), [])
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MCPResourceMeta,
|
|
3
|
+
MCPToolMeta,
|
|
4
|
+
MCPPromptMeta,
|
|
5
|
+
} from '@pikku/core/mcp'
|
|
6
|
+
|
|
7
|
+
export interface MCPToolEndpoint {
|
|
8
|
+
name: string
|
|
9
|
+
title: string
|
|
10
|
+
description: string
|
|
11
|
+
inputSchema?: any
|
|
12
|
+
outputSchema?: any
|
|
13
|
+
enabled: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MCPResourceEndpoint {
|
|
17
|
+
uri: string
|
|
18
|
+
title: string
|
|
19
|
+
description: string
|
|
20
|
+
inputSchema?: any
|
|
21
|
+
mimeType?: string
|
|
22
|
+
enabled: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MCPPromptEndpoint {
|
|
26
|
+
name: string
|
|
27
|
+
description: string
|
|
28
|
+
inputSchema?: any
|
|
29
|
+
enabled: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class MCPEndpointRegistry {
|
|
33
|
+
private tools: Map<string, MCPToolEndpoint> = new Map()
|
|
34
|
+
private resources: Map<string, MCPResourceEndpoint> = new Map()
|
|
35
|
+
private prompts: Map<string, MCPPromptEndpoint> = new Map()
|
|
36
|
+
private resourcesMeta: MCPResourceMeta = {}
|
|
37
|
+
private toolsMeta: MCPToolMeta = {}
|
|
38
|
+
private promptsMeta: MCPPromptMeta = {}
|
|
39
|
+
|
|
40
|
+
async loadFromMCPJson(mcpData: any): Promise<void> {
|
|
41
|
+
if (mcpData.tools && Array.isArray(mcpData.tools)) {
|
|
42
|
+
for (const tool of mcpData.tools) {
|
|
43
|
+
this.tools.set(tool.name, {
|
|
44
|
+
name: tool.name,
|
|
45
|
+
title: tool.title,
|
|
46
|
+
description: tool.description,
|
|
47
|
+
inputSchema: tool.parameters ?? { type: 'object' },
|
|
48
|
+
outputSchema: tool.returns,
|
|
49
|
+
enabled: tool.enabled !== undefined ? tool.enabled : true,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (mcpData.resources && Array.isArray(mcpData.resources)) {
|
|
54
|
+
for (const resource of mcpData.resources) {
|
|
55
|
+
this.resources.set(resource.name, {
|
|
56
|
+
title: resource.name,
|
|
57
|
+
uri: resource.uri,
|
|
58
|
+
description: resource.description,
|
|
59
|
+
inputSchema: resource.parameters,
|
|
60
|
+
enabled: resource.enabled !== undefined ? resource.enabled : true,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (mcpData.prompts && Array.isArray(mcpData.prompts)) {
|
|
65
|
+
for (const prompt of mcpData.prompts) {
|
|
66
|
+
this.prompts.set(prompt.name, {
|
|
67
|
+
name: prompt.name,
|
|
68
|
+
description: prompt.description,
|
|
69
|
+
inputSchema: prompt.arguments,
|
|
70
|
+
enabled: prompt.enabled !== undefined ? prompt.enabled : true,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setResourcesMeta(meta: MCPResourceMeta): void {
|
|
77
|
+
this.resourcesMeta = meta
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setToolsMeta(meta: MCPToolMeta): void {
|
|
81
|
+
this.toolsMeta = meta
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setPromptsMeta(meta: MCPPromptMeta): void {
|
|
85
|
+
this.promptsMeta = meta
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getTools(): MCPToolEndpoint[] {
|
|
89
|
+
return Array.from(this.tools.values()).filter((tool) => tool.enabled)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getResources(): MCPResourceEndpoint[] {
|
|
93
|
+
return Array.from(this.resources.values())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getPrompts(): MCPPromptEndpoint[] {
|
|
97
|
+
return Array.from(this.prompts.values())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getTool(name: string): MCPToolEndpoint | undefined {
|
|
101
|
+
return this.tools.get(name)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getResource(name: string): MCPResourceEndpoint | undefined {
|
|
105
|
+
return this.resources.get(name)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getPrompt(name: string): MCPPromptEndpoint | undefined {
|
|
109
|
+
return this.prompts.get(name)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
hasResource(name: string): boolean {
|
|
113
|
+
return name in this.resourcesMeta
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
hasTool(name: string): boolean {
|
|
117
|
+
return name in this.toolsMeta
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
hasPrompt(name: string): boolean {
|
|
121
|
+
return name in this.promptsMeta
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
enableTools(tools: Record<any, boolean>): boolean {
|
|
125
|
+
let changed = false
|
|
126
|
+
for (const [name, enabled] of Object.entries(tools)) {
|
|
127
|
+
const tool = this.tools.get(name)
|
|
128
|
+
if (tool && tool.enabled !== enabled) {
|
|
129
|
+
tool.enabled = enabled
|
|
130
|
+
changed = true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return changed
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
enablePrompts(prompts: Record<any, boolean>): boolean {
|
|
137
|
+
let changed = false
|
|
138
|
+
for (const [name, enabled] of Object.entries(prompts)) {
|
|
139
|
+
const prompt = this.prompts.get(name)
|
|
140
|
+
if (prompt && prompt.enabled !== enabled) {
|
|
141
|
+
prompt.enabled = enabled
|
|
142
|
+
changed = true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return changed
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
enableResources(resources: Record<any, boolean>): boolean {
|
|
149
|
+
let changed = false
|
|
150
|
+
for (const [name, enabled] of Object.entries(resources)) {
|
|
151
|
+
const resource = this.resources.get(name)
|
|
152
|
+
if (resource && resource.enabled !== enabled) {
|
|
153
|
+
resource.enabled = enabled
|
|
154
|
+
changed = true
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return changed
|
|
158
|
+
}
|
|
159
|
+
}
|