spiceflow 1.12.1 → 1.13.0
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/README.md +107 -4
- package/dist/_node-server-unsupported.js +1 -0
- package/dist/_node-server-unsupported.js.map +1 -0
- package/dist/_node-server.d.ts.map +1 -1
- package/dist/_node-server.js +2 -2
- package/dist/_node-server.js.map +1 -0
- package/dist/client/errors.js +1 -0
- package/dist/client/errors.js.map +1 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +7 -4
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.js +1 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/utils.js +1 -0
- package/dist/client/utils.js.map +1 -0
- package/dist/client.test.js +1 -0
- package/dist/client.test.js.map +1 -0
- package/dist/context.d.ts +6 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +1 -0
- package/dist/context.js.map +1 -0
- package/dist/cors.js +1 -0
- package/dist/cors.js.map +1 -0
- package/dist/cors.test.js +1 -0
- package/dist/cors.test.js.map +1 -0
- package/dist/error.js +1 -0
- package/dist/error.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client-transport.d.ts +35 -0
- package/dist/mcp-client-transport.d.ts.map +1 -0
- package/dist/mcp-client-transport.js +147 -0
- package/dist/mcp-client-transport.js.map +1 -0
- package/dist/mcp-transport.js +1 -0
- package/dist/mcp-transport.js.map +1 -0
- package/dist/mcp.d.ts +18 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +43 -224
- package/dist/mcp.js.map +1 -0
- package/dist/middleware.test.js +1 -0
- package/dist/middleware.test.js.map +1 -0
- package/dist/openapi-to-mcp.d.ts +38 -0
- package/dist/openapi-to-mcp.d.ts.map +1 -0
- package/dist/openapi-to-mcp.js +367 -0
- package/dist/openapi-to-mcp.js.map +1 -0
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +7 -2
- package/dist/openapi.js.map +1 -0
- package/dist/openapi.test.js +32 -31
- package/dist/openapi.test.js.map +1 -0
- package/dist/simple.benchmark.js +1 -0
- package/dist/simple.benchmark.js.map +1 -0
- package/dist/spiceflow.d.ts +5 -2
- package/dist/spiceflow.d.ts.map +1 -1
- package/dist/spiceflow.js +26 -6
- package/dist/spiceflow.js.map +1 -0
- package/dist/spiceflow.test.js +15 -3
- package/dist/spiceflow.test.js.map +1 -0
- package/dist/static-node.js +1 -0
- package/dist/static-node.js.map +1 -0
- package/dist/static.benchmark.js +1 -0
- package/dist/static.benchmark.js.map +1 -0
- package/dist/static.js +1 -0
- package/dist/static.js.map +1 -0
- package/dist/stream.test.js +1 -0
- package/dist/stream.test.js.map +1 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/types.test.js +1 -0
- package/dist/types.test.js.map +1 -0
- package/dist/utils.js +1 -0
- package/dist/utils.js.map +1 -0
- package/dist/waitUntil.test.d.ts +2 -0
- package/dist/waitUntil.test.d.ts.map +1 -0
- package/dist/waitUntil.test.js +142 -0
- package/dist/waitUntil.test.js.map +1 -0
- package/dist/zod.test.js +1 -0
- package/dist/zod.test.js.map +1 -0
- package/package.json +4 -3
- package/src/_node-server.ts +1 -2
- package/src/client/index.ts +9 -7
- package/src/context.ts +6 -3
- package/src/index.ts +1 -1
- package/src/mcp-client-transport.ts +184 -0
- package/src/mcp.ts +49 -307
- package/src/openapi-to-mcp.ts +510 -0
- package/src/openapi.test.ts +31 -31
- package/src/openapi.ts +9 -3
- package/src/spiceflow.test.ts +18 -4
- package/src/spiceflow.ts +42 -15
- package/src/waitUntil.test.ts +168 -0
- package/dist/serialize.d.ts +0 -2
- package/dist/serialize.d.ts.map +0 -1
- package/dist/serialize.js +0 -9
- package/src/serialize.ts +0 -10
package/src/mcp.ts
CHANGED
|
@@ -1,290 +1,35 @@
|
|
|
1
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
CallToolRequestSchema,
|
|
5
|
-
ListResourcesRequestSchema,
|
|
6
|
-
ListToolsRequestSchema,
|
|
7
|
-
ReadResourceRequestSchema,
|
|
8
|
-
} from '@modelcontextprotocol/sdk/types.js'
|
|
9
1
|
import { OpenAPIV3 } from 'openapi-types'
|
|
10
2
|
import { SSEServerTransportSpiceflow } from './mcp-transport.ts'
|
|
3
|
+
import { createMCPServer } from './openapi-to-mcp.ts'
|
|
11
4
|
import { openapi } from './openapi.ts'
|
|
12
5
|
import { Spiceflow } from './spiceflow.ts'
|
|
13
6
|
|
|
14
|
-
const
|
|
15
|
-
function getOperationRequestBody(
|
|
16
|
-
operation: OpenAPIV3.OperationObject,
|
|
17
|
-
): OpenAPIV3.SchemaObject | undefined {
|
|
18
|
-
if (!operation.requestBody) return undefined
|
|
19
|
-
|
|
20
|
-
const requestBody = operation.requestBody as OpenAPIV3.RequestBodyObject
|
|
21
|
-
const content = requestBody.content['application/json']
|
|
22
|
-
return content?.schema as OpenAPIV3.SchemaObject
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getOperationParameters(operation: OpenAPIV3.OperationObject): {
|
|
26
|
-
queryParams?: OpenAPIV3.SchemaObject
|
|
27
|
-
pathParams?: OpenAPIV3.SchemaObject
|
|
28
|
-
} {
|
|
29
|
-
if (!operation.parameters) return {}
|
|
30
|
-
|
|
31
|
-
const queryProperties: Record<string, OpenAPIV3.SchemaObject> = {}
|
|
32
|
-
const pathProperties: Record<string, OpenAPIV3.SchemaObject> = {}
|
|
33
|
-
const queryRequired: string[] = []
|
|
34
|
-
const pathRequired: string[] = []
|
|
35
|
-
|
|
36
|
-
operation.parameters.forEach((param) => {
|
|
37
|
-
if ('$ref' in param) return // TODO referenced parameters
|
|
38
|
-
|
|
39
|
-
if (param.in === 'query') {
|
|
40
|
-
queryProperties[param.name] = param.schema as OpenAPIV3.SchemaObject
|
|
41
|
-
if (param.required) queryRequired.push(param.name)
|
|
42
|
-
} else if (param.in === 'path') {
|
|
43
|
-
pathProperties[param.name] = param.schema as OpenAPIV3.SchemaObject
|
|
44
|
-
if (param.required) pathRequired.push(param.name)
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
const result: {
|
|
49
|
-
queryParams?: OpenAPIV3.SchemaObject
|
|
50
|
-
pathParams?: OpenAPIV3.SchemaObject
|
|
51
|
-
} = {}
|
|
52
|
-
|
|
53
|
-
if (Object.keys(queryProperties).length > 0) {
|
|
54
|
-
result.queryParams = {
|
|
55
|
-
type: 'object',
|
|
56
|
-
properties: queryProperties,
|
|
57
|
-
required: queryRequired.length > 0 ? queryRequired : undefined,
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (Object.keys(pathProperties).length > 0) {
|
|
62
|
-
result.pathParams = {
|
|
63
|
-
type: 'object',
|
|
64
|
-
properties: pathProperties,
|
|
65
|
-
required: pathRequired.length > 0 ? pathRequired : undefined,
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return result
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function createMCPServer({
|
|
73
|
-
name = 'spiceflow',
|
|
74
|
-
version = '1.0.0',
|
|
75
|
-
openapi,
|
|
76
|
-
app,
|
|
77
|
-
}: {
|
|
78
|
-
name?: string
|
|
79
|
-
version?: string
|
|
80
|
-
openapi: OpenAPIV3.Document
|
|
81
|
-
app: Spiceflow
|
|
82
|
-
}) {
|
|
83
|
-
const server = new Server(
|
|
84
|
-
{ name, version },
|
|
85
|
-
{
|
|
86
|
-
capabilities: {
|
|
87
|
-
tools: {},
|
|
88
|
-
resources: {},
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
const basePath = app.topLevelApp!.prefix || ''
|
|
94
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
95
|
-
const paths = Object.entries(openapi.paths).filter(
|
|
96
|
-
([path]) =>
|
|
97
|
-
!['/mcp-openapi', '/mcp', '/mcp/message'].includes(
|
|
98
|
-
path.replace(basePath, ''),
|
|
99
|
-
),
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
const tools = paths.flatMap(([path, pathObj]) =>
|
|
103
|
-
Object.entries(pathObj || {})
|
|
104
|
-
.filter(([method]) => method !== 'parameters')
|
|
105
|
-
.map(([method, operation]) => {
|
|
106
|
-
const properties: Record<string, any> = {}
|
|
107
|
-
const required: string[] = []
|
|
108
|
-
|
|
109
|
-
const requestBody = getOperationRequestBody(
|
|
110
|
-
operation as OpenAPIV3.OperationObject,
|
|
111
|
-
)
|
|
112
|
-
if (requestBody) {
|
|
113
|
-
properties.body = requestBody
|
|
114
|
-
required.push('body')
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const { queryParams, pathParams } = getOperationParameters(
|
|
118
|
-
operation as OpenAPIV3.OperationObject,
|
|
119
|
-
)
|
|
120
|
-
if (queryParams) {
|
|
121
|
-
properties.query = queryParams
|
|
122
|
-
}
|
|
123
|
-
if (pathParams) {
|
|
124
|
-
properties.params = pathParams
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
name: getRouteName({ method, path }),
|
|
129
|
-
description:
|
|
130
|
-
(operation as OpenAPIV3.OperationObject).description ||
|
|
131
|
-
(operation as OpenAPIV3.OperationObject).summary ||
|
|
132
|
-
`${method.toUpperCase()} ${path}`,
|
|
133
|
-
inputSchema: {
|
|
134
|
-
type: 'object',
|
|
135
|
-
properties,
|
|
136
|
-
required: required.length > 0 ? required : undefined,
|
|
137
|
-
},
|
|
138
|
-
}
|
|
139
|
-
}),
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
return { tools }
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
146
|
-
const toolName = request.params.name
|
|
147
|
-
let { path, method } = getPathFromToolName(toolName)
|
|
148
|
-
|
|
149
|
-
const pathObj = openapi.paths[path]
|
|
150
|
-
if (!pathObj || !pathObj[method.toLowerCase()]) {
|
|
151
|
-
return {
|
|
152
|
-
content: [{ type: 'text', text: `Tool ${toolName} not found` }],
|
|
153
|
-
isError: true,
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
const { body, query, params } = request.params.arguments || {}
|
|
159
|
-
|
|
160
|
-
if (params) {
|
|
161
|
-
Object.entries(params).forEach(([key, value]) => {
|
|
162
|
-
path = path.replace(`{${key}}`, encodeURIComponent(String(value)))
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const basePath = app.topLevelApp!.prefix || ''
|
|
167
|
-
const url = new URL(`http://localhost${basePath}${path}`)
|
|
168
|
-
if (query) {
|
|
169
|
-
Object.entries(query).forEach(([key, value]) => {
|
|
170
|
-
url.searchParams.set(key, String(value))
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const response = await app.handle(
|
|
175
|
-
new Request(url, {
|
|
176
|
-
method: method,
|
|
177
|
-
headers: {
|
|
178
|
-
'content-type': 'application/json',
|
|
179
|
-
},
|
|
180
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
181
|
-
}),
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
const isError = !response.ok
|
|
185
|
-
const contentType = response.headers.get('content-type')
|
|
186
|
-
|
|
187
|
-
if (contentType?.includes('application/json')) {
|
|
188
|
-
const json = await response.json()
|
|
189
|
-
return {
|
|
190
|
-
isError,
|
|
191
|
-
content: [{ type: 'text', text: JSON.stringify(json, null, 2) }],
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const text = await response.text()
|
|
196
|
-
return {
|
|
197
|
-
isError,
|
|
198
|
-
content: [{ type: 'text', text }],
|
|
199
|
-
}
|
|
200
|
-
} catch (error: any) {
|
|
201
|
-
return {
|
|
202
|
-
content: [{ type: 'text', text: error.message || 'Unknown error' }],
|
|
203
|
-
isError: true,
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
209
|
-
const resources: { uri: string; mimeType: string; name: string }[] = []
|
|
210
|
-
for (const [path, pathObj] of Object.entries(openapi.paths)) {
|
|
211
|
-
if (path.startsWith('/mcp')) {
|
|
212
|
-
continue
|
|
213
|
-
}
|
|
214
|
-
const getOperation = pathObj?.get as OpenAPIV3.OperationObject
|
|
215
|
-
if (getOperation && !path.includes('{')) {
|
|
216
|
-
const { queryParams } = getOperationParameters(getOperation)
|
|
217
|
-
const hasRequiredQuery =
|
|
218
|
-
queryParams?.required && queryParams.required.length > 0
|
|
219
|
-
|
|
220
|
-
if (!hasRequiredQuery) {
|
|
221
|
-
resources.push({
|
|
222
|
-
uri: new URL(path, 'http://localhost').href,
|
|
223
|
-
mimeType: 'application/json',
|
|
224
|
-
name: `GET ${path}`,
|
|
225
|
-
})
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return { resources }
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
233
|
-
const resourceUrl = new URL(request.params.uri)
|
|
234
|
-
const path = resourceUrl.pathname
|
|
235
|
-
|
|
236
|
-
const pathObj = openapi.paths[path]
|
|
237
|
-
if (!pathObj?.get) {
|
|
238
|
-
throw new Error('Resource not found')
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const response = await app.handle(
|
|
242
|
-
new Request(resourceUrl, {
|
|
243
|
-
method: 'GET',
|
|
244
|
-
headers: {
|
|
245
|
-
'content-type': 'application/json',
|
|
246
|
-
},
|
|
247
|
-
}),
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
const contentType = response.headers.get('content-type')
|
|
251
|
-
const text = await response.text()
|
|
252
|
-
|
|
253
|
-
if (contentType?.includes('application/json')) {
|
|
254
|
-
return {
|
|
255
|
-
contents: [
|
|
256
|
-
{
|
|
257
|
-
uri: request.params.uri,
|
|
258
|
-
mimeType: 'application/json',
|
|
259
|
-
text: text,
|
|
260
|
-
},
|
|
261
|
-
],
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return {
|
|
266
|
-
contents: [
|
|
267
|
-
{
|
|
268
|
-
uri: request.params.uri,
|
|
269
|
-
mimeType: 'text/plain',
|
|
270
|
-
text,
|
|
271
|
-
},
|
|
272
|
-
],
|
|
273
|
-
}
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
return { server, transports }
|
|
277
|
-
}
|
|
7
|
+
const defaultTransports = new Map<string, SSEServerTransportSpiceflow>()
|
|
278
8
|
|
|
279
9
|
export const mcp = <Path extends string = '/mcp'>({
|
|
280
10
|
path = '/mcp' as Path,
|
|
281
11
|
name = 'spiceflow',
|
|
282
12
|
version = '1.0.0',
|
|
13
|
+
/**
|
|
14
|
+
* Map to get a transport from a sessionId and
|
|
15
|
+
*/
|
|
16
|
+
transports = defaultTransports,
|
|
283
17
|
} = {}) => {
|
|
284
18
|
const messagePath = path + '/message'
|
|
285
19
|
|
|
286
20
|
let app = new Spiceflow({ name: 'mcp' })
|
|
287
|
-
.use(openapi({ path: '/
|
|
21
|
+
.use(openapi({ path: '/_mcp_openapi' }))
|
|
22
|
+
.route({
|
|
23
|
+
method: 'GET',
|
|
24
|
+
path: '/_mcp_config',
|
|
25
|
+
handler: async () => {
|
|
26
|
+
return {
|
|
27
|
+
name,
|
|
28
|
+
version,
|
|
29
|
+
path,
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
})
|
|
288
33
|
.post(messagePath, async ({ request, query }) => {
|
|
289
34
|
const sessionId = query.sessionId!
|
|
290
35
|
|
|
@@ -297,22 +42,46 @@ export const mcp = <Path extends string = '/mcp'>({
|
|
|
297
42
|
return 'ok'
|
|
298
43
|
})
|
|
299
44
|
.get(path, async ({ request }) => {
|
|
300
|
-
const basePath = app.topLevelApp!.
|
|
45
|
+
const basePath = app.topLevelApp!.basePath || ''
|
|
301
46
|
const transport = new SSEServerTransportSpiceflow(basePath + messagePath)
|
|
302
47
|
transports.set(transport.sessionId, transport)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
48
|
+
|
|
49
|
+
const [openapi, mcpConfig] = await Promise.all([
|
|
50
|
+
app
|
|
51
|
+
.topLevelApp!.handle(
|
|
52
|
+
new Request(`http://localhost${basePath}/_mcp_openapi`),
|
|
53
|
+
)
|
|
54
|
+
.then((r) => r.json()) as Promise<OpenAPIV3.Document>,
|
|
55
|
+
app
|
|
56
|
+
.topLevelApp!.handle(
|
|
57
|
+
new Request(`http://localhost${basePath}/_mcp_config`),
|
|
58
|
+
)
|
|
59
|
+
.then((r) => r.json()),
|
|
60
|
+
])
|
|
61
|
+
const mcpPath = mcpConfig?.path
|
|
62
|
+
if (!mcpPath)
|
|
63
|
+
throw new Error(
|
|
64
|
+
'Missing MCP path from app, make sure to use the mcp() Spiceflow plugin',
|
|
306
65
|
)
|
|
307
|
-
.then((r) => r.json())
|
|
308
66
|
const { server } = createMCPServer({
|
|
309
67
|
name,
|
|
310
68
|
version,
|
|
69
|
+
ignorePaths: [
|
|
70
|
+
'/_mcp_openapi',
|
|
71
|
+
'/_mcp_config',
|
|
72
|
+
mcpPath,
|
|
73
|
+
mcpPath + '/message', //
|
|
74
|
+
],
|
|
75
|
+
|
|
76
|
+
fetch: (url, init) => {
|
|
77
|
+
const req = new Request(url, init)
|
|
78
|
+
return app.handle(req)
|
|
79
|
+
},
|
|
311
80
|
openapi,
|
|
312
|
-
app: app!.topLevelApp!,
|
|
313
81
|
})
|
|
82
|
+
|
|
314
83
|
server.onclose = () => {
|
|
315
|
-
transports.delete(transport.sessionId)
|
|
84
|
+
// transports.delete(transport.sessionId)
|
|
316
85
|
}
|
|
317
86
|
await server.connect(transport)
|
|
318
87
|
|
|
@@ -322,35 +91,8 @@ export const mcp = <Path extends string = '/mcp'>({
|
|
|
322
91
|
})
|
|
323
92
|
})
|
|
324
93
|
|
|
325
|
-
if (request.method === 'POST') {
|
|
326
|
-
return await transport.handlePostMessage(request)
|
|
327
|
-
}
|
|
328
|
-
|
|
329
94
|
return transport.response
|
|
330
95
|
})
|
|
331
96
|
|
|
332
97
|
return app
|
|
333
98
|
}
|
|
334
|
-
|
|
335
|
-
function getRouteName({
|
|
336
|
-
method,
|
|
337
|
-
path,
|
|
338
|
-
}: {
|
|
339
|
-
method: string
|
|
340
|
-
path: string
|
|
341
|
-
}): string {
|
|
342
|
-
return `${method.toUpperCase()} ${path}`
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function getPathFromToolName(toolName: string): {
|
|
346
|
-
path: string
|
|
347
|
-
method: string
|
|
348
|
-
} {
|
|
349
|
-
const parts = toolName.split(' ')
|
|
350
|
-
if (parts.length < 2) {
|
|
351
|
-
throw new Error('Invalid tool name format')
|
|
352
|
-
}
|
|
353
|
-
const method = parts[0].toUpperCase()
|
|
354
|
-
const path = parts.slice(1).join(' ')
|
|
355
|
-
return { path, method }
|
|
356
|
-
}
|