spiceflow 1.6.1 → 1.6.2
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 +73 -7
- package/dist/client.test.js +11 -4
- package/dist/client.test.js.map +1 -1
- package/dist/cors.d.ts.map +1 -1
- package/dist/cors.js +13 -2
- package/dist/cors.js.map +1 -1
- package/dist/error.d.ts +2 -2
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +1 -1
- package/dist/error.js.map +1 -1
- package/dist/mcp.d.ts +12 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +204 -147
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.js +4 -13
- package/dist/mcp.test.js.map +1 -1
- package/dist/openapi.d.ts +0 -5
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +0 -5
- package/dist/openapi.js.map +1 -1
- package/dist/openapi.test.js +2 -2
- package/dist/spiceflow.d.ts +2 -1
- package/dist/spiceflow.d.ts.map +1 -1
- package/dist/spiceflow.js +14 -2
- package/dist/spiceflow.js.map +1 -1
- package/dist/spiceflow.test.js +17 -1
- package/dist/spiceflow.test.js.map +1 -1
- package/dist/stream.test.js +20 -8
- package/dist/stream.test.js.map +1 -1
- package/dist/types.d.ts +4 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -2
- package/src/client.test.ts +11 -4
- package/src/cors.ts +17 -5
- package/src/error.ts +2 -2
- package/src/mcp.test.ts +4 -13
- package/src/mcp.ts +263 -178
- package/src/openapi.test.ts +2 -2
- package/src/openapi.ts +0 -5
- package/src/spiceflow.test.ts +20 -1
- package/src/spiceflow.ts +20 -4
- package/src/stream.test.ts +21 -8
- package/src/types.ts +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spiceflow",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "Simple API framework with RPC and type safety",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"ajv": "^8.17.1",
|
|
46
46
|
"ajv-formats": "^3.0.1",
|
|
47
47
|
"eventsource-parser": "^3.0.0",
|
|
48
|
+
"lodash.clonedeep": "^4.5.0",
|
|
48
49
|
"openapi-types": "^12.1.3",
|
|
49
50
|
"superjson": "^2.2.2",
|
|
50
51
|
"zod": "^3.24.1",
|
|
@@ -59,7 +60,8 @@
|
|
|
59
60
|
}
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
|
-
"@types/
|
|
63
|
+
"@types/lodash.clonedeep": "^4.5.9",
|
|
64
|
+
"@types/node": "22.13.4",
|
|
63
65
|
"eventsource": "^3.0.5",
|
|
64
66
|
"formdata-node": "^6.0.3",
|
|
65
67
|
"js-base64": "^3.7.7",
|
package/src/client.test.ts
CHANGED
|
@@ -234,12 +234,19 @@ describe('client', () => {
|
|
|
234
234
|
|
|
235
235
|
it('stream return', async () => {
|
|
236
236
|
const { data } = await client['stream-return'].get()
|
|
237
|
-
|
|
237
|
+
let all = ''
|
|
238
|
+
for await (const chunk of data!) {
|
|
239
|
+
all += chunk
|
|
240
|
+
}
|
|
241
|
+
expect(all).toEqual('a')
|
|
238
242
|
})
|
|
239
243
|
it('stream return async', async () => {
|
|
240
|
-
const { data } = await client['stream-return-async'].get(
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
const { data } = await client['stream-return-async'].get()
|
|
245
|
+
let all = ''
|
|
246
|
+
for await (const chunk of data!) {
|
|
247
|
+
all += chunk
|
|
248
|
+
}
|
|
249
|
+
expect(all).toEqual('a')
|
|
243
250
|
})
|
|
244
251
|
it('post zodAny', async () => {
|
|
245
252
|
const body = [{ key: 'value' }, 123, 'string', true, null]
|
package/src/cors.ts
CHANGED
|
@@ -27,7 +27,7 @@ export const cors = (options?: CORSOptions): MiddlewareHandler => {
|
|
|
27
27
|
allowHeaders: [],
|
|
28
28
|
exposeHeaders: [],
|
|
29
29
|
credentials: true,
|
|
30
|
-
cacheAge: 21600 // 6 hours default
|
|
30
|
+
cacheAge: 21600, // 6 hours default
|
|
31
31
|
}
|
|
32
32
|
const opts = {
|
|
33
33
|
...defaults,
|
|
@@ -49,7 +49,16 @@ export const cors = (options?: CORSOptions): MiddlewareHandler => {
|
|
|
49
49
|
let response = await next()
|
|
50
50
|
|
|
51
51
|
function set(key: string, value: string) {
|
|
52
|
-
|
|
52
|
+
try {
|
|
53
|
+
response.headers.set(key, value)
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error instanceof TypeError && error.message.includes('immutable')) {
|
|
56
|
+
response = new Response(response.body, response)
|
|
57
|
+
set(key, value)
|
|
58
|
+
} else {
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
}
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
const allowOrigin = findAllowOrigin(c.request.headers.get('origin') || '')
|
|
@@ -82,10 +91,13 @@ export const cors = (options?: CORSOptions): MiddlewareHandler => {
|
|
|
82
91
|
if (opts.cacheAge && opts.cacheAge > 0) {
|
|
83
92
|
// CORS preflight cache
|
|
84
93
|
set('Access-Control-Max-Age', opts.cacheAge.toString())
|
|
85
|
-
|
|
94
|
+
|
|
86
95
|
// Browser cache and CDN cache
|
|
87
|
-
set(
|
|
88
|
-
|
|
96
|
+
set(
|
|
97
|
+
'Cache-Control',
|
|
98
|
+
`public, max-age=${opts.cacheAge}, s-maxage=${opts.cacheAge}`,
|
|
99
|
+
)
|
|
100
|
+
|
|
89
101
|
// Additional CDN-specific headers
|
|
90
102
|
set('CDN-Cache-Control', `public, s-maxage=${opts.cacheAge}`)
|
|
91
103
|
set('Cloudflare-CDN-Cache-Control', `public, s-maxage=${opts.cacheAge}`)
|
package/src/error.ts
CHANGED
|
@@ -11,8 +11,8 @@ const env =
|
|
|
11
11
|
export const ERROR_CODE = Symbol('SpiceflowErrorCode')
|
|
12
12
|
export type ERROR_CODE = typeof ERROR_CODE
|
|
13
13
|
|
|
14
|
-
export const
|
|
15
|
-
export type
|
|
14
|
+
export const SPICEFLOW_RESPONSE = Symbol('SpiceflowResponse')
|
|
15
|
+
export type SPICEFLOW_RESPONSE = typeof SPICEFLOW_RESPONSE
|
|
16
16
|
|
|
17
17
|
export const isProduction = (env?.NODE_ENV ?? env?.ENV) === 'production'
|
|
18
18
|
|
package/src/mcp.test.ts
CHANGED
|
@@ -85,7 +85,6 @@ describe('MCP Plugin', () => {
|
|
|
85
85
|
"description": "GET /goSomething",
|
|
86
86
|
"inputSchema": {
|
|
87
87
|
"properties": {},
|
|
88
|
-
"required": [],
|
|
89
88
|
"type": "object",
|
|
90
89
|
},
|
|
91
90
|
"name": "GET /goSomething",
|
|
@@ -94,18 +93,15 @@ describe('MCP Plugin', () => {
|
|
|
94
93
|
"description": "GET /users",
|
|
95
94
|
"inputSchema": {
|
|
96
95
|
"properties": {},
|
|
97
|
-
"required": [],
|
|
98
96
|
"type": "object",
|
|
99
97
|
},
|
|
100
98
|
"name": "GET /users",
|
|
101
99
|
},
|
|
102
100
|
{
|
|
103
|
-
"description": "GET /somethingElse
|
|
101
|
+
"description": "GET /somethingElse/{id}",
|
|
104
102
|
"inputSchema": {
|
|
105
103
|
"properties": {
|
|
106
104
|
"params": {
|
|
107
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
108
|
-
"additionalProperties": false,
|
|
109
105
|
"properties": {
|
|
110
106
|
"id": {
|
|
111
107
|
"type": "string",
|
|
@@ -117,24 +113,20 @@ describe('MCP Plugin', () => {
|
|
|
117
113
|
"type": "object",
|
|
118
114
|
},
|
|
119
115
|
},
|
|
120
|
-
"required": [],
|
|
121
116
|
"type": "object",
|
|
122
117
|
},
|
|
123
|
-
"name": "GET /somethingElse
|
|
118
|
+
"name": "GET /somethingElse/{id}",
|
|
124
119
|
},
|
|
125
120
|
{
|
|
126
121
|
"description": "GET /search",
|
|
127
122
|
"inputSchema": {
|
|
128
123
|
"properties": {
|
|
129
124
|
"query": {
|
|
130
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
131
|
-
"additionalProperties": false,
|
|
132
125
|
"properties": {
|
|
133
126
|
"limit": {
|
|
134
127
|
"type": "number",
|
|
135
128
|
},
|
|
136
129
|
"q": {
|
|
137
|
-
"description": "Search query",
|
|
138
130
|
"type": "string",
|
|
139
131
|
},
|
|
140
132
|
},
|
|
@@ -145,7 +137,6 @@ describe('MCP Plugin', () => {
|
|
|
145
137
|
"type": "object",
|
|
146
138
|
},
|
|
147
139
|
},
|
|
148
|
-
"required": [],
|
|
149
140
|
"type": "object",
|
|
150
141
|
},
|
|
151
142
|
"name": "GET /search",
|
|
@@ -195,12 +186,12 @@ describe('MCP Plugin', () => {
|
|
|
195
186
|
{
|
|
196
187
|
"mimeType": "application/json",
|
|
197
188
|
"name": "GET /goSomething",
|
|
198
|
-
"uri": "http://localhost
|
|
189
|
+
"uri": "http://localhost/goSomething",
|
|
199
190
|
},
|
|
200
191
|
{
|
|
201
192
|
"mimeType": "application/json",
|
|
202
193
|
"name": "GET /users",
|
|
203
|
-
"uri": "http://localhost
|
|
194
|
+
"uri": "http://localhost/users",
|
|
204
195
|
},
|
|
205
196
|
]
|
|
206
197
|
`)
|
package/src/mcp.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema'
|
|
|
10
10
|
import { SSEServerTransportSpiceflow } from './mcp-transport.js'
|
|
11
11
|
import { isZodSchema, Spiceflow } from './spiceflow.js'
|
|
12
12
|
import { OpenAPIV3 } from 'openapi-types'
|
|
13
|
+
import { openapi } from './openapi.js'
|
|
13
14
|
|
|
14
15
|
function getJsonSchema(schema: any) {
|
|
15
16
|
if (!schema) return undefined
|
|
@@ -20,12 +21,75 @@ function getJsonSchema(schema: any) {
|
|
|
20
21
|
}
|
|
21
22
|
return schema
|
|
22
23
|
}
|
|
24
|
+
const transports = new Map<string, SSEServerTransportSpiceflow>()
|
|
25
|
+
function getOperationRequestBody(
|
|
26
|
+
operation: OpenAPIV3.OperationObject,
|
|
27
|
+
): OpenAPIV3.SchemaObject | undefined {
|
|
28
|
+
if (!operation.requestBody) return undefined
|
|
29
|
+
|
|
30
|
+
const requestBody = operation.requestBody as OpenAPIV3.RequestBodyObject
|
|
31
|
+
const content = requestBody.content['application/json']
|
|
32
|
+
return content?.schema as OpenAPIV3.SchemaObject
|
|
33
|
+
}
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
function getOperationParameters(operation: OpenAPIV3.OperationObject): {
|
|
36
|
+
queryParams?: OpenAPIV3.SchemaObject
|
|
37
|
+
pathParams?: OpenAPIV3.SchemaObject
|
|
38
|
+
} {
|
|
39
|
+
if (!operation.parameters) return {}
|
|
40
|
+
|
|
41
|
+
const queryProperties: Record<string, OpenAPIV3.SchemaObject> = {}
|
|
42
|
+
const pathProperties: Record<string, OpenAPIV3.SchemaObject> = {}
|
|
43
|
+
const queryRequired: string[] = []
|
|
44
|
+
const pathRequired: string[] = []
|
|
45
|
+
|
|
46
|
+
operation.parameters.forEach((param) => {
|
|
47
|
+
if ('$ref' in param) return // TODO referenced parameters
|
|
48
|
+
|
|
49
|
+
if (param.in === 'query') {
|
|
50
|
+
queryProperties[param.name] = param.schema as OpenAPIV3.SchemaObject
|
|
51
|
+
if (param.required) queryRequired.push(param.name)
|
|
52
|
+
} else if (param.in === 'path') {
|
|
53
|
+
pathProperties[param.name] = param.schema as OpenAPIV3.SchemaObject
|
|
54
|
+
if (param.required) pathRequired.push(param.name)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const result: {
|
|
59
|
+
queryParams?: OpenAPIV3.SchemaObject
|
|
60
|
+
pathParams?: OpenAPIV3.SchemaObject
|
|
61
|
+
} = {}
|
|
62
|
+
|
|
63
|
+
if (Object.keys(queryProperties).length > 0) {
|
|
64
|
+
result.queryParams = {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: queryProperties,
|
|
67
|
+
required: queryRequired.length > 0 ? queryRequired : undefined,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (Object.keys(pathProperties).length > 0) {
|
|
72
|
+
result.pathParams = {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: pathProperties,
|
|
75
|
+
required: pathRequired.length > 0 ? pathRequired : undefined,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createMCPServer({
|
|
26
83
|
name = 'spiceflow',
|
|
27
84
|
version = '1.0.0',
|
|
28
|
-
|
|
85
|
+
openapi,
|
|
86
|
+
app,
|
|
87
|
+
}: {
|
|
88
|
+
name?: string
|
|
89
|
+
version?: string
|
|
90
|
+
openapi: OpenAPIV3.Document
|
|
91
|
+
app: Spiceflow
|
|
92
|
+
}) {
|
|
29
93
|
const server = new Server(
|
|
30
94
|
{ name, version },
|
|
31
95
|
{
|
|
@@ -36,213 +100,234 @@ export const mcp = <Path extends string = '/mcp'>({
|
|
|
36
100
|
},
|
|
37
101
|
)
|
|
38
102
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
await t.handlePostMessage(request)
|
|
51
|
-
return 'ok'
|
|
52
|
-
})
|
|
53
|
-
.get(path, async ({ request }) => {
|
|
54
|
-
const transport = new SSEServerTransportSpiceflow(messagePath)
|
|
55
|
-
transports.set(transport.sessionId, transport)
|
|
56
|
-
server.onclose = () => {
|
|
57
|
-
transports.delete(transport.sessionId)
|
|
58
|
-
}
|
|
59
|
-
await server.connect(transport)
|
|
60
|
-
|
|
61
|
-
request.signal.addEventListener('abort', () => {
|
|
62
|
-
transport.close().catch((error) => {
|
|
63
|
-
console.error('Error closing transport:', error)
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
if (request.method === 'POST') {
|
|
68
|
-
return await transport.handlePostMessage(request)
|
|
69
|
-
}
|
|
70
|
-
let routes = app
|
|
71
|
-
.getAllRoutes()
|
|
72
|
-
.filter((x) => x.path !== path && x.path !== messagePath)
|
|
73
|
-
|
|
74
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
75
|
-
return {
|
|
76
|
-
tools: routes.map((route) => {
|
|
77
|
-
const bodySchema = getJsonSchema(route.hooks?.body)
|
|
78
|
-
const querySchema = getJsonSchema(route.hooks?.query)
|
|
79
|
-
const paramsSchema = getJsonSchema(route.hooks?.params)
|
|
80
|
-
|
|
103
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
104
|
+
const tools = Object.entries(openapi.paths)
|
|
105
|
+
.filter(
|
|
106
|
+
([path]) => !['/mcp-openapi', '/mcp', '/mcp/message'].includes(path),
|
|
107
|
+
)
|
|
108
|
+
.flatMap(([path, pathObj]) =>
|
|
109
|
+
Object.entries(pathObj || {})
|
|
110
|
+
.filter(([method]) => method !== 'parameters')
|
|
111
|
+
.map(([method, operation]) => {
|
|
81
112
|
const properties: Record<string, any> = {}
|
|
82
113
|
const required: string[] = []
|
|
83
114
|
|
|
84
|
-
|
|
85
|
-
|
|
115
|
+
const requestBody = getOperationRequestBody(
|
|
116
|
+
operation as OpenAPIV3.OperationObject,
|
|
117
|
+
)
|
|
118
|
+
if (requestBody) {
|
|
119
|
+
properties.body = requestBody
|
|
86
120
|
required.push('body')
|
|
87
121
|
}
|
|
88
|
-
|
|
89
|
-
|
|
122
|
+
|
|
123
|
+
const { queryParams, pathParams } = getOperationParameters(
|
|
124
|
+
operation as OpenAPIV3.OperationObject,
|
|
125
|
+
)
|
|
126
|
+
if (queryParams) {
|
|
127
|
+
properties.query = queryParams
|
|
90
128
|
}
|
|
91
|
-
if (
|
|
92
|
-
properties.params =
|
|
129
|
+
if (pathParams) {
|
|
130
|
+
properties.params = pathParams
|
|
93
131
|
}
|
|
94
132
|
|
|
95
133
|
return {
|
|
96
|
-
name: getRouteName({ method
|
|
97
|
-
|
|
134
|
+
name: getRouteName({ method, path }),
|
|
98
135
|
description:
|
|
99
|
-
|
|
100
|
-
|
|
136
|
+
(operation as OpenAPIV3.OperationObject).description ||
|
|
137
|
+
(operation as OpenAPIV3.OperationObject).summary ||
|
|
138
|
+
`${method.toUpperCase()} ${path}`,
|
|
101
139
|
inputSchema: {
|
|
102
140
|
type: 'object',
|
|
103
141
|
properties,
|
|
104
|
-
required,
|
|
142
|
+
required: required.length > 0 ? required : undefined,
|
|
105
143
|
},
|
|
106
144
|
}
|
|
107
145
|
}),
|
|
108
|
-
|
|
109
|
-
})
|
|
146
|
+
)
|
|
110
147
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
let { path, method } = getPathFromToolName(toolName)
|
|
148
|
+
return { tools }
|
|
149
|
+
})
|
|
114
150
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)
|
|
151
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
152
|
+
const toolName = request.params.name
|
|
153
|
+
let { path, method } = getPathFromToolName(toolName)
|
|
119
154
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
const { body, query, params } = request.params.arguments || {}
|
|
129
|
-
|
|
130
|
-
if (params) {
|
|
131
|
-
Object.entries(params).forEach(([key, value]) => {
|
|
132
|
-
path = path.replace(`:${key}`, encodeURIComponent(String(value)))
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
const url = new URL(`http://localhost${path}`)
|
|
136
|
-
if (query) {
|
|
137
|
-
Object.entries(query).forEach(([key, value]) => {
|
|
138
|
-
url.searchParams.set(key, String(value))
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const response = await app.topLevelApp!.handle(
|
|
143
|
-
new Request(url, {
|
|
144
|
-
method: route.method,
|
|
145
|
-
headers: {
|
|
146
|
-
'content-type': 'application/json',
|
|
147
|
-
},
|
|
148
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
149
|
-
}),
|
|
150
|
-
)
|
|
155
|
+
const pathObj = openapi.paths[path]
|
|
156
|
+
if (!pathObj || !pathObj[method.toLowerCase()]) {
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: 'text', text: `Tool ${toolName} not found` }],
|
|
159
|
+
isError: true,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
151
162
|
|
|
152
|
-
|
|
163
|
+
try {
|
|
164
|
+
const { body, query, params } = request.params.arguments || {}
|
|
153
165
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
content: [{ type: 'text', text: JSON.stringify(json, null, 2) }],
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const text = await response.text()
|
|
164
|
-
return {
|
|
165
|
-
isError,
|
|
166
|
-
content: [{ type: 'text', text }],
|
|
167
|
-
}
|
|
168
|
-
} catch (error: any) {
|
|
169
|
-
return {
|
|
170
|
-
content: [{ type: 'text', text: error.message || 'Unknown error' }],
|
|
171
|
-
isError: true,
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
|
-
const resourcesRoutes = routes.filter((route) => {
|
|
176
|
-
if (route.method !== 'GET') return false
|
|
166
|
+
if (params) {
|
|
167
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
168
|
+
path = path.replace(`{${key}}`, encodeURIComponent(String(value)))
|
|
169
|
+
})
|
|
170
|
+
}
|
|
177
171
|
|
|
178
|
-
|
|
172
|
+
const url = new URL(`http://localhost${path}`)
|
|
173
|
+
if (query) {
|
|
174
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
175
|
+
url.searchParams.set(key, String(value))
|
|
176
|
+
})
|
|
177
|
+
}
|
|
179
178
|
|
|
180
|
-
|
|
179
|
+
const response = await app.handle(
|
|
180
|
+
new Request(url, {
|
|
181
|
+
method: method,
|
|
182
|
+
headers: {
|
|
183
|
+
'content-type': 'application/json',
|
|
184
|
+
},
|
|
185
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
186
|
+
}),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
const isError = !response.ok
|
|
190
|
+
const contentType = response.headers.get('content-type')
|
|
191
|
+
|
|
192
|
+
if (contentType?.includes('application/json')) {
|
|
193
|
+
const json = await response.json()
|
|
194
|
+
return {
|
|
195
|
+
isError,
|
|
196
|
+
content: [{ type: 'text', text: JSON.stringify(json, null, 2) }],
|
|
197
|
+
}
|
|
198
|
+
}
|
|
181
199
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
200
|
+
const text = await response.text()
|
|
201
|
+
return {
|
|
202
|
+
isError,
|
|
203
|
+
content: [{ type: 'text', text }],
|
|
204
|
+
}
|
|
205
|
+
} catch (error: any) {
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: 'text', text: error.message || 'Unknown error' }],
|
|
208
|
+
isError: true,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
214
|
+
const resources: { uri: string; mimeType: string; name: string }[] = []
|
|
215
|
+
for (const [path, pathObj] of Object.entries(openapi.paths)) {
|
|
216
|
+
if (path.startsWith('/mcp')) {
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
const getOperation = pathObj?.get as OpenAPIV3.OperationObject
|
|
220
|
+
if (getOperation && !path.includes('{')) {
|
|
221
|
+
const { queryParams } = getOperationParameters(getOperation)
|
|
222
|
+
const hasRequiredQuery =
|
|
223
|
+
queryParams?.required && queryParams.required.length > 0
|
|
224
|
+
|
|
225
|
+
if (!hasRequiredQuery) {
|
|
226
|
+
resources.push({
|
|
227
|
+
uri: new URL(path, 'http://localhost').href,
|
|
228
|
+
mimeType: 'application/json',
|
|
229
|
+
name: `GET ${path}`,
|
|
230
|
+
})
|
|
187
231
|
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { resources }
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
238
|
+
const resourceUrl = new URL(request.params.uri)
|
|
239
|
+
const path = resourceUrl.pathname
|
|
240
|
+
|
|
241
|
+
const pathObj = openapi.paths[path]
|
|
242
|
+
if (!pathObj?.get) {
|
|
243
|
+
throw new Error('Resource not found')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const response = await app.handle(
|
|
247
|
+
new Request(resourceUrl, {
|
|
248
|
+
method: 'GET',
|
|
249
|
+
headers: {
|
|
250
|
+
'content-type': 'application/json',
|
|
251
|
+
},
|
|
252
|
+
}),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
const contentType = response.headers.get('content-type')
|
|
256
|
+
const text = await response.text()
|
|
257
|
+
|
|
258
|
+
if (contentType?.includes('application/json')) {
|
|
259
|
+
return {
|
|
260
|
+
contents: [
|
|
261
|
+
{
|
|
262
|
+
uri: request.params.uri,
|
|
263
|
+
mimeType: 'application/json',
|
|
264
|
+
text: text,
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
contents: [
|
|
272
|
+
{
|
|
273
|
+
uri: request.params.uri,
|
|
274
|
+
mimeType: 'text/plain',
|
|
275
|
+
text,
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
return { server, transports }
|
|
282
|
+
}
|
|
188
283
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
mimeType: 'application/json',
|
|
196
|
-
name: `GET ${route.path}`,
|
|
197
|
-
}))
|
|
198
|
-
return { resources }
|
|
199
|
-
})
|
|
284
|
+
export const mcp = <Path extends string = '/mcp'>({
|
|
285
|
+
path = '/mcp' as Path,
|
|
286
|
+
name = 'spiceflow',
|
|
287
|
+
version = '1.0.0',
|
|
288
|
+
} = {}) => {
|
|
289
|
+
const messagePath = path + '/message'
|
|
200
290
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
291
|
+
let app = new Spiceflow({ name: 'mcp' })
|
|
292
|
+
.use(openapi({ path: '/mcp-openapi' }))
|
|
293
|
+
.post(messagePath, async ({ request, query }) => {
|
|
294
|
+
const sessionId = query.sessionId!
|
|
204
295
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
throw new Error('Resource not found')
|
|
210
|
-
}
|
|
296
|
+
const t = transports.get(sessionId)
|
|
297
|
+
if (!t) {
|
|
298
|
+
return new Response('Session not found', { status: 404 })
|
|
299
|
+
}
|
|
211
300
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
],
|
|
232
|
-
}
|
|
233
|
-
}
|
|
301
|
+
await t.handlePostMessage(request)
|
|
302
|
+
return 'ok'
|
|
303
|
+
})
|
|
304
|
+
.get(path, async ({ request }) => {
|
|
305
|
+
const transport = new SSEServerTransportSpiceflow(messagePath)
|
|
306
|
+
transports.set(transport.sessionId, transport)
|
|
307
|
+
const openapi = await app
|
|
308
|
+
.topLevelApp!.handle(new Request('http://localhost/mcp-openapi'))
|
|
309
|
+
.then((r) => r.json())
|
|
310
|
+
const { server } = createMCPServer({
|
|
311
|
+
name,
|
|
312
|
+
version,
|
|
313
|
+
openapi,
|
|
314
|
+
app: app!.topLevelApp!,
|
|
315
|
+
})
|
|
316
|
+
server.onclose = () => {
|
|
317
|
+
transports.delete(transport.sessionId)
|
|
318
|
+
}
|
|
319
|
+
await server.connect(transport)
|
|
234
320
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
mimeType: 'text/plain',
|
|
240
|
-
text,
|
|
241
|
-
},
|
|
242
|
-
],
|
|
243
|
-
}
|
|
321
|
+
request.signal.addEventListener('abort', () => {
|
|
322
|
+
transport.close().catch((error) => {
|
|
323
|
+
console.error('Error closing transport:', error)
|
|
324
|
+
})
|
|
244
325
|
})
|
|
245
326
|
|
|
327
|
+
if (request.method === 'POST') {
|
|
328
|
+
return await transport.handlePostMessage(request)
|
|
329
|
+
}
|
|
330
|
+
|
|
246
331
|
return transport.response
|
|
247
332
|
})
|
|
248
333
|
|