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.
Files changed (98) hide show
  1. package/README.md +107 -4
  2. package/dist/_node-server-unsupported.js +1 -0
  3. package/dist/_node-server-unsupported.js.map +1 -0
  4. package/dist/_node-server.d.ts.map +1 -1
  5. package/dist/_node-server.js +2 -2
  6. package/dist/_node-server.js.map +1 -0
  7. package/dist/client/errors.js +1 -0
  8. package/dist/client/errors.js.map +1 -0
  9. package/dist/client/index.d.ts +2 -2
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +7 -4
  12. package/dist/client/index.js.map +1 -0
  13. package/dist/client/types.js +1 -0
  14. package/dist/client/types.js.map +1 -0
  15. package/dist/client/utils.js +1 -0
  16. package/dist/client/utils.js.map +1 -0
  17. package/dist/client.test.js +1 -0
  18. package/dist/client.test.js.map +1 -0
  19. package/dist/context.d.ts +6 -3
  20. package/dist/context.d.ts.map +1 -1
  21. package/dist/context.js +1 -0
  22. package/dist/context.js.map +1 -0
  23. package/dist/cors.js +1 -0
  24. package/dist/cors.js.map +1 -0
  25. package/dist/cors.test.js +1 -0
  26. package/dist/cors.test.js.map +1 -0
  27. package/dist/error.js +1 -0
  28. package/dist/error.js.map +1 -0
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/mcp-client-transport.d.ts +35 -0
  34. package/dist/mcp-client-transport.d.ts.map +1 -0
  35. package/dist/mcp-client-transport.js +147 -0
  36. package/dist/mcp-client-transport.js.map +1 -0
  37. package/dist/mcp-transport.js +1 -0
  38. package/dist/mcp-transport.js.map +1 -0
  39. package/dist/mcp.d.ts +18 -1
  40. package/dist/mcp.d.ts.map +1 -1
  41. package/dist/mcp.js +43 -224
  42. package/dist/mcp.js.map +1 -0
  43. package/dist/middleware.test.js +1 -0
  44. package/dist/middleware.test.js.map +1 -0
  45. package/dist/openapi-to-mcp.d.ts +38 -0
  46. package/dist/openapi-to-mcp.d.ts.map +1 -0
  47. package/dist/openapi-to-mcp.js +367 -0
  48. package/dist/openapi-to-mcp.js.map +1 -0
  49. package/dist/openapi.d.ts.map +1 -1
  50. package/dist/openapi.js +7 -2
  51. package/dist/openapi.js.map +1 -0
  52. package/dist/openapi.test.js +32 -31
  53. package/dist/openapi.test.js.map +1 -0
  54. package/dist/simple.benchmark.js +1 -0
  55. package/dist/simple.benchmark.js.map +1 -0
  56. package/dist/spiceflow.d.ts +5 -2
  57. package/dist/spiceflow.d.ts.map +1 -1
  58. package/dist/spiceflow.js +26 -6
  59. package/dist/spiceflow.js.map +1 -0
  60. package/dist/spiceflow.test.js +15 -3
  61. package/dist/spiceflow.test.js.map +1 -0
  62. package/dist/static-node.js +1 -0
  63. package/dist/static-node.js.map +1 -0
  64. package/dist/static.benchmark.js +1 -0
  65. package/dist/static.benchmark.js.map +1 -0
  66. package/dist/static.js +1 -0
  67. package/dist/static.js.map +1 -0
  68. package/dist/stream.test.js +1 -0
  69. package/dist/stream.test.js.map +1 -0
  70. package/dist/types.js +1 -0
  71. package/dist/types.js.map +1 -0
  72. package/dist/types.test.js +1 -0
  73. package/dist/types.test.js.map +1 -0
  74. package/dist/utils.js +1 -0
  75. package/dist/utils.js.map +1 -0
  76. package/dist/waitUntil.test.d.ts +2 -0
  77. package/dist/waitUntil.test.d.ts.map +1 -0
  78. package/dist/waitUntil.test.js +142 -0
  79. package/dist/waitUntil.test.js.map +1 -0
  80. package/dist/zod.test.js +1 -0
  81. package/dist/zod.test.js.map +1 -0
  82. package/package.json +4 -3
  83. package/src/_node-server.ts +1 -2
  84. package/src/client/index.ts +9 -7
  85. package/src/context.ts +6 -3
  86. package/src/index.ts +1 -1
  87. package/src/mcp-client-transport.ts +184 -0
  88. package/src/mcp.ts +49 -307
  89. package/src/openapi-to-mcp.ts +510 -0
  90. package/src/openapi.test.ts +31 -31
  91. package/src/openapi.ts +9 -3
  92. package/src/spiceflow.test.ts +18 -4
  93. package/src/spiceflow.ts +42 -15
  94. package/src/waitUntil.test.ts +168 -0
  95. package/dist/serialize.d.ts +0 -2
  96. package/dist/serialize.d.ts.map +0 -1
  97. package/dist/serialize.js +0 -9
  98. 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 transports = new Map<string, SSEServerTransportSpiceflow>()
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: '/mcp-openapi' }))
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!.prefix || ''
45
+ const basePath = app.topLevelApp!.basePath || ''
301
46
  const transport = new SSEServerTransportSpiceflow(basePath + messagePath)
302
47
  transports.set(transport.sessionId, transport)
303
- const openapi = await app
304
- .topLevelApp!.handle(
305
- new Request(`http://localhost${basePath}/mcp-openapi`),
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
- }