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
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
2
|
+
// import deref from 'dereference-json-schema'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListResourcesRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
ReadResourceRequestSchema,
|
|
9
|
+
} from '@modelcontextprotocol/sdk/types.js'
|
|
10
|
+
import { OpenAPIV3 } from 'openapi-types'
|
|
11
|
+
|
|
12
|
+
function getOperationRequestBody(
|
|
13
|
+
operation: OpenAPIV3.OperationObject,
|
|
14
|
+
): OpenAPIV3.SchemaObject | undefined {
|
|
15
|
+
if (!operation.requestBody) return undefined
|
|
16
|
+
|
|
17
|
+
const requestBody = operation.requestBody as OpenAPIV3.RequestBodyObject
|
|
18
|
+
const content = requestBody.content['application/json']
|
|
19
|
+
return content?.schema as OpenAPIV3.SchemaObject
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getOperationParameters(operation: OpenAPIV3.OperationObject): {
|
|
23
|
+
queryParams?: OpenAPIV3.SchemaObject
|
|
24
|
+
pathParams?: OpenAPIV3.SchemaObject
|
|
25
|
+
headerParams?: OpenAPIV3.SchemaObject
|
|
26
|
+
cookieParams?: OpenAPIV3.SchemaObject
|
|
27
|
+
} {
|
|
28
|
+
if (!operation.parameters) return {}
|
|
29
|
+
|
|
30
|
+
const queryProperties: Record<string, OpenAPIV3.SchemaObject> = {}
|
|
31
|
+
const pathProperties: Record<string, OpenAPIV3.SchemaObject> = {}
|
|
32
|
+
const headerProperties: Record<string, OpenAPIV3.SchemaObject> = {}
|
|
33
|
+
const cookieProperties: Record<string, OpenAPIV3.SchemaObject> = {}
|
|
34
|
+
const queryRequired: string[] = []
|
|
35
|
+
const pathRequired: string[] = []
|
|
36
|
+
const headerRequired: string[] = []
|
|
37
|
+
const cookieRequired: string[] = []
|
|
38
|
+
|
|
39
|
+
operation.parameters.forEach((param) => {
|
|
40
|
+
const paramObj = param as OpenAPIV3.ParameterObject
|
|
41
|
+
if (paramObj.in === 'query') {
|
|
42
|
+
queryProperties[paramObj.name] = paramObj.schema as OpenAPIV3.SchemaObject
|
|
43
|
+
if (paramObj.required) queryRequired.push(paramObj.name)
|
|
44
|
+
} else if (paramObj.in === 'path') {
|
|
45
|
+
pathProperties[paramObj.name] = paramObj.schema as OpenAPIV3.SchemaObject
|
|
46
|
+
if (paramObj.required) pathRequired.push(paramObj.name)
|
|
47
|
+
} else if (paramObj.in === 'header') {
|
|
48
|
+
headerProperties[paramObj.name] =
|
|
49
|
+
paramObj.schema as OpenAPIV3.SchemaObject
|
|
50
|
+
if (paramObj.required) headerRequired.push(paramObj.name)
|
|
51
|
+
} else if (paramObj.in === 'cookie') {
|
|
52
|
+
cookieProperties[paramObj.name] =
|
|
53
|
+
paramObj.schema as OpenAPIV3.SchemaObject
|
|
54
|
+
if (paramObj.required) cookieRequired.push(paramObj.name)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const result: {
|
|
59
|
+
queryParams?: OpenAPIV3.SchemaObject
|
|
60
|
+
pathParams?: OpenAPIV3.SchemaObject
|
|
61
|
+
headerParams?: OpenAPIV3.SchemaObject
|
|
62
|
+
cookieParams?: OpenAPIV3.SchemaObject
|
|
63
|
+
} = {}
|
|
64
|
+
|
|
65
|
+
if (Object.keys(queryProperties).length > 0) {
|
|
66
|
+
result.queryParams = {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: queryProperties,
|
|
69
|
+
required: queryRequired.length > 0 ? queryRequired : undefined,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (Object.keys(pathProperties).length > 0) {
|
|
74
|
+
result.pathParams = {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: pathProperties,
|
|
77
|
+
required: pathRequired.length > 0 ? pathRequired : undefined,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (Object.keys(headerProperties).length > 0) {
|
|
82
|
+
result.headerParams = {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: headerProperties,
|
|
85
|
+
required: headerRequired.length > 0 ? headerRequired : undefined,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Object.keys(cookieProperties).length > 0) {
|
|
90
|
+
result.cookieParams = {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: cookieProperties,
|
|
93
|
+
required: cookieRequired.length > 0 ? cookieRequired : undefined,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
}
|
|
99
|
+
function extractApiFromBaseUrl(openapi: OpenAPIV3.Document): string {
|
|
100
|
+
if (openapi.servers && openapi.servers.length > 0) {
|
|
101
|
+
return openapi.servers[0].url
|
|
102
|
+
}
|
|
103
|
+
return ''
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getAuthHeaders(
|
|
107
|
+
openapi: OpenAPIV3.Document,
|
|
108
|
+
operation?: OpenAPIV3.OperationObject,
|
|
109
|
+
): Record<string, string> {
|
|
110
|
+
const headers: Record<string, string> = {}
|
|
111
|
+
const token = process.env.API_TOKEN
|
|
112
|
+
|
|
113
|
+
if (!token || !openapi.components?.securitySchemes) {
|
|
114
|
+
return headers
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const securitySchemes = openapi.components.securitySchemes
|
|
118
|
+
let selectedScheme: OpenAPIV3.SecuritySchemeObject | null = null
|
|
119
|
+
|
|
120
|
+
// Check for operation-specific security requirements first
|
|
121
|
+
if (operation?.security && operation.security.length > 0) {
|
|
122
|
+
const firstSecurityReq = operation.security[0]
|
|
123
|
+
const operationSchemeNames = Object.keys(firstSecurityReq)
|
|
124
|
+
|
|
125
|
+
for (const schemeName of operationSchemeNames) {
|
|
126
|
+
const scheme = securitySchemes[schemeName]
|
|
127
|
+
if (scheme) {
|
|
128
|
+
selectedScheme = scheme as OpenAPIV3.SecuritySchemeObject
|
|
129
|
+
break
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If no operation-specific scheme found, use the first available scheme
|
|
135
|
+
if (!selectedScheme) {
|
|
136
|
+
const schemes = Object.values(securitySchemes)
|
|
137
|
+
if (schemes.length > 0) {
|
|
138
|
+
selectedScheme = schemes[0] as OpenAPIV3.SecuritySchemeObject
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!selectedScheme) {
|
|
143
|
+
return headers
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Set headers based on scheme type
|
|
147
|
+
switch (selectedScheme.type) {
|
|
148
|
+
case 'http':
|
|
149
|
+
if (selectedScheme.scheme === 'bearer') {
|
|
150
|
+
headers['Authorization'] = `Bearer ${token}`
|
|
151
|
+
} else if (selectedScheme.scheme === 'basic') {
|
|
152
|
+
headers['Authorization'] = `Basic ${token}`
|
|
153
|
+
}
|
|
154
|
+
break
|
|
155
|
+
case 'apiKey':
|
|
156
|
+
if (selectedScheme.in === 'header') {
|
|
157
|
+
headers[selectedScheme.name] = token
|
|
158
|
+
}
|
|
159
|
+
break
|
|
160
|
+
case 'oauth2':
|
|
161
|
+
headers['Authorization'] = `Bearer ${token}`
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return headers
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
169
|
+
const defaultFetch = fetch
|
|
170
|
+
|
|
171
|
+
export function createMCPServer({
|
|
172
|
+
name = 'spiceflow',
|
|
173
|
+
version = '1.0.0',
|
|
174
|
+
openapi,
|
|
175
|
+
|
|
176
|
+
fetch = defaultFetch,
|
|
177
|
+
paths,
|
|
178
|
+
ignorePaths,
|
|
179
|
+
baseUrl = '',
|
|
180
|
+
}: {
|
|
181
|
+
name?: string
|
|
182
|
+
version?: string
|
|
183
|
+
|
|
184
|
+
fetch?: Fetch
|
|
185
|
+
openapi: OpenAPIV3.Document
|
|
186
|
+
paths?: string[]
|
|
187
|
+
ignorePaths?: string[]
|
|
188
|
+
baseUrl?: string
|
|
189
|
+
}) {
|
|
190
|
+
const server = new Server(
|
|
191
|
+
{ name, version },
|
|
192
|
+
{
|
|
193
|
+
capabilities: {
|
|
194
|
+
tools: {},
|
|
195
|
+
resources: {},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
// openapi = deref.dereferenceSync(openapi)
|
|
200
|
+
if (!baseUrl) {
|
|
201
|
+
baseUrl = extractApiFromBaseUrl(openapi)
|
|
202
|
+
}
|
|
203
|
+
if (baseUrl.endsWith('/')) {
|
|
204
|
+
baseUrl = baseUrl.slice(0, -1)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function fetchWithBaseServerAndAuth(
|
|
208
|
+
u: string,
|
|
209
|
+
options: RequestInit,
|
|
210
|
+
operation?: OpenAPIV3.OperationObject,
|
|
211
|
+
userHeaders?: Record<string, string>,
|
|
212
|
+
userCookies?: Record<string, string>,
|
|
213
|
+
) {
|
|
214
|
+
const authHeaders = getAuthHeaders(openapi, operation)
|
|
215
|
+
|
|
216
|
+
// Build cookie string from userCookies
|
|
217
|
+
let cookieHeader = ''
|
|
218
|
+
if (userCookies && Object.keys(userCookies).length > 0) {
|
|
219
|
+
cookieHeader = Object.entries(userCookies)
|
|
220
|
+
.map(
|
|
221
|
+
([key, value]) =>
|
|
222
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
|
|
223
|
+
)
|
|
224
|
+
.join('; ')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const commonHeaders: Record<string, string> = {}
|
|
228
|
+
|
|
229
|
+
const finalHeaders: Record<string, string> = {
|
|
230
|
+
...commonHeaders,
|
|
231
|
+
...(userHeaders || {}),
|
|
232
|
+
...authHeaders,
|
|
233
|
+
...((options?.headers as Record<string, string>) || {}),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (cookieHeader) {
|
|
237
|
+
// Merge with existing Cookie header if present
|
|
238
|
+
const existingCookie = finalHeaders['Cookie'] || finalHeaders['cookie']
|
|
239
|
+
finalHeaders['Cookie'] = existingCookie
|
|
240
|
+
? `${existingCookie}; ${cookieHeader}`
|
|
241
|
+
: cookieHeader
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.error(`using headers ${JSON.stringify(finalHeaders, null, 2)}`)
|
|
245
|
+
|
|
246
|
+
return await fetch!(new URL(u, baseUrl), {
|
|
247
|
+
...options,
|
|
248
|
+
headers: finalHeaders,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
253
|
+
const filteredPaths = Object.entries(openapi.paths).filter(([path]) => {
|
|
254
|
+
if (ignorePaths?.includes(path)) return false
|
|
255
|
+
if (paths && paths.length > 0) {
|
|
256
|
+
return paths.some((filterPath) => path.startsWith(filterPath))
|
|
257
|
+
}
|
|
258
|
+
return true
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const tools = filteredPaths.flatMap(([path, pathObj]) =>
|
|
262
|
+
Object.entries(pathObj || {})
|
|
263
|
+
.filter(([method]) => method !== 'parameters')
|
|
264
|
+
.map(([method, operation]) => {
|
|
265
|
+
const properties: Record<string, any> = {}
|
|
266
|
+
const required: string[] = []
|
|
267
|
+
|
|
268
|
+
const requestBody = getOperationRequestBody(
|
|
269
|
+
operation as OpenAPIV3.OperationObject,
|
|
270
|
+
)
|
|
271
|
+
if (requestBody) {
|
|
272
|
+
properties.body = requestBody
|
|
273
|
+
required.push('body')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const { queryParams, pathParams, headerParams, cookieParams } =
|
|
277
|
+
getOperationParameters(operation as OpenAPIV3.OperationObject)
|
|
278
|
+
if (queryParams) {
|
|
279
|
+
properties.query = queryParams
|
|
280
|
+
}
|
|
281
|
+
if (pathParams) {
|
|
282
|
+
properties.params = pathParams
|
|
283
|
+
}
|
|
284
|
+
if (headerParams) {
|
|
285
|
+
properties.headers = headerParams
|
|
286
|
+
}
|
|
287
|
+
if (cookieParams) {
|
|
288
|
+
properties.cookies = cookieParams
|
|
289
|
+
}
|
|
290
|
+
let description = `${method.toUpperCase()} route for ${baseUrl}${path}`
|
|
291
|
+
let moreDescription =
|
|
292
|
+
(operation as OpenAPIV3.OperationObject).description ||
|
|
293
|
+
(operation as OpenAPIV3.OperationObject).summary
|
|
294
|
+
if (moreDescription) {
|
|
295
|
+
description += '. '
|
|
296
|
+
description += moreDescription
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
name: getRouteName({ method, path }),
|
|
301
|
+
description,
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
properties,
|
|
305
|
+
required: required.length > 0 ? required : undefined,
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
}),
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
return { tools }
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
315
|
+
const toolName = request.params.name
|
|
316
|
+
let { path, method } = getPathFromToolName(toolName)
|
|
317
|
+
|
|
318
|
+
const pathObj = openapi.paths[path]
|
|
319
|
+
if (!pathObj || !pathObj[method.toLowerCase()]) {
|
|
320
|
+
return {
|
|
321
|
+
content: [{ type: 'text', text: `Tool ${toolName} not found` }],
|
|
322
|
+
isError: true,
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const args = request.params.arguments || {}
|
|
328
|
+
const { body, query, params } = args
|
|
329
|
+
const userHeaders = args.headers as Record<string, string> | undefined
|
|
330
|
+
const userCookies = args.cookies as Record<string, string> | undefined
|
|
331
|
+
const operation = pathObj[
|
|
332
|
+
method.toLowerCase()
|
|
333
|
+
] as OpenAPIV3.OperationObject
|
|
334
|
+
|
|
335
|
+
if (params) {
|
|
336
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
337
|
+
path = path.replace(`{${key}}`, encodeURIComponent(String(value)))
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (query) {
|
|
342
|
+
const searchParams = new URLSearchParams()
|
|
343
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
344
|
+
searchParams.set(key, String(value))
|
|
345
|
+
})
|
|
346
|
+
path += `?${searchParams.toString()}`
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const response = await fetchWithBaseServerAndAuth(
|
|
350
|
+
path,
|
|
351
|
+
{
|
|
352
|
+
method,
|
|
353
|
+
headers: {
|
|
354
|
+
'content-type': 'application/json',
|
|
355
|
+
},
|
|
356
|
+
body: body
|
|
357
|
+
? typeof body === 'string'
|
|
358
|
+
? body
|
|
359
|
+
: JSON.stringify(body)
|
|
360
|
+
: undefined,
|
|
361
|
+
},
|
|
362
|
+
operation,
|
|
363
|
+
userHeaders,
|
|
364
|
+
userCookies,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
const isError = !response.ok
|
|
368
|
+
const contentType = response.headers.get('content-type')
|
|
369
|
+
|
|
370
|
+
if (contentType?.includes('application/json')) {
|
|
371
|
+
const json = await response.json()
|
|
372
|
+
return {
|
|
373
|
+
isError,
|
|
374
|
+
content: [{ type: 'text', text: JSON.stringify(json, null, 2) }],
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const text = await response.text()
|
|
379
|
+
return {
|
|
380
|
+
isError,
|
|
381
|
+
content: [{ type: 'text', text }],
|
|
382
|
+
}
|
|
383
|
+
} catch (error: any) {
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: 'text', text: error.message || 'Unknown error' }],
|
|
386
|
+
isError: true,
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
392
|
+
const resources: { uri: string; mimeType: string; name: string }[] = []
|
|
393
|
+
for (const [path, pathObj] of Object.entries(openapi.paths)) {
|
|
394
|
+
if (path.startsWith('/mcp')) {
|
|
395
|
+
continue
|
|
396
|
+
}
|
|
397
|
+
const getOperation = pathObj?.get as OpenAPIV3.OperationObject
|
|
398
|
+
if (getOperation && !path.includes('{')) {
|
|
399
|
+
const { queryParams, headerParams, cookieParams } =
|
|
400
|
+
getOperationParameters(getOperation)
|
|
401
|
+
const hasRequiredQuery =
|
|
402
|
+
queryParams?.required && queryParams.required.length > 0
|
|
403
|
+
const hasRequiredHeaders =
|
|
404
|
+
headerParams?.required && headerParams.required.length > 0
|
|
405
|
+
const hasRequiredCookies =
|
|
406
|
+
cookieParams?.required && cookieParams.required.length > 0
|
|
407
|
+
|
|
408
|
+
if (!hasRequiredQuery && !hasRequiredHeaders && !hasRequiredCookies) {
|
|
409
|
+
resources.push({
|
|
410
|
+
uri: path,
|
|
411
|
+
mimeType: 'application/json',
|
|
412
|
+
name: `GET ${path}`,
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return { resources: [] }
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
421
|
+
throw new Error('Resources are not supported - use tools instead')
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
return { server }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function getRouteName({
|
|
428
|
+
method,
|
|
429
|
+
path,
|
|
430
|
+
}: {
|
|
431
|
+
method: string
|
|
432
|
+
path: string
|
|
433
|
+
}): string {
|
|
434
|
+
return formatToolName(`${method.toUpperCase()} ${path}`, method, path)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const toolNameToPath = new Map<string, { method: string; path: string }>()
|
|
438
|
+
|
|
439
|
+
function getPathFromToolName(toolName: string): {
|
|
440
|
+
path: string
|
|
441
|
+
method: string
|
|
442
|
+
} {
|
|
443
|
+
const cached = toolNameToPath.get(toolName)
|
|
444
|
+
if (cached) {
|
|
445
|
+
return cached
|
|
446
|
+
}
|
|
447
|
+
throw new Error(
|
|
448
|
+
`Tool name '${toolName}' not found. It might not have been registered or was invalid.`,
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function formatToolName(
|
|
453
|
+
nameToFormat: string,
|
|
454
|
+
method: string,
|
|
455
|
+
pathForMap: string,
|
|
456
|
+
): string {
|
|
457
|
+
if (!nameToFormat || nameToFormat.trim() === '') {
|
|
458
|
+
throw new Error('Original tool name for formatting cannot be empty')
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Replace spaces and other invalid characters with underscores
|
|
462
|
+
let formatted = nameToFormat
|
|
463
|
+
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
464
|
+
.replace(/_+/g, '_') // Replace multiple underscores with single underscore
|
|
465
|
+
.replace(/^_+|_+$/g, '') // Remove leading/trailing underscores
|
|
466
|
+
|
|
467
|
+
// Truncate to 64 characters if necessary
|
|
468
|
+
if (formatted.length > 64) {
|
|
469
|
+
formatted = formatted.substring(0, 64)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Remove trailing underscores again in case truncation created them or they persisted
|
|
473
|
+
formatted = formatted.replace(/_+$/, '')
|
|
474
|
+
|
|
475
|
+
if (formatted === '') {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`Tool name results in empty string after formatting (from original: '${nameToFormat}')`,
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Validate against regex
|
|
482
|
+
const regex = /^[a-zA-Z0-9_-]{1,64}$/
|
|
483
|
+
if (!regex.test(formatted)) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`Formatted tool name "${formatted}" (from original: '${nameToFormat}') does not match required pattern: ^[a-zA-Z0-9_-]{1,64}$`,
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Check for duplicates: if this formatted name already exists and belongs to a DIFFERENT tool (method/path), it's a collision.
|
|
490
|
+
const existingEntry = toolNameToPath.get(formatted)
|
|
491
|
+
if (
|
|
492
|
+
existingEntry &&
|
|
493
|
+
(existingEntry.method !== method || existingEntry.path !== pathForMap)
|
|
494
|
+
) {
|
|
495
|
+
console.error(
|
|
496
|
+
new Error(
|
|
497
|
+
`Duplicate tool name generated: '${formatted}'. ` +
|
|
498
|
+
`This name was generated for original: '${nameToFormat}' (method: '${method}', path: '${pathForMap}'). ` +
|
|
499
|
+
`It conflicts with an existing tool that also maps to '${formatted}', originally from (method: '${existingEntry.method}', path: '${existingEntry.path}'). ` +
|
|
500
|
+
`Ensure operationIds or path/method combinations in your OpenAPI spec are sufficiently unique to avoid naming collisions after formatting.`,
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Register the name with its method and path.
|
|
506
|
+
// If the same tool (method/path) is formatted again to the same name, this just overwrites with identical values.
|
|
507
|
+
toolNameToPath.set(formatted, { method, path: pathForMap })
|
|
508
|
+
|
|
509
|
+
return formatted
|
|
510
|
+
}
|
package/src/openapi.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { openapi } from './openapi.ts'
|
|
|
5
5
|
import { Spiceflow } from './spiceflow.ts'
|
|
6
6
|
|
|
7
7
|
test('openapi response', async () => {
|
|
8
|
-
const app = new Spiceflow()
|
|
8
|
+
const app = new Spiceflow({ basePath: '/base' })
|
|
9
9
|
.use(
|
|
10
10
|
openapi({
|
|
11
11
|
info: {
|
|
@@ -156,73 +156,73 @@ test('openapi response', async () => {
|
|
|
156
156
|
),
|
|
157
157
|
)
|
|
158
158
|
const openapiSchema = await app
|
|
159
|
-
.handle(new Request('http://localhost/openapi'))
|
|
159
|
+
.handle(new Request('http://localhost/base/openapi'))
|
|
160
160
|
.then((x) => x.json())
|
|
161
161
|
expect(openapiSchema).toMatchInlineSnapshot(`
|
|
162
162
|
{
|
|
163
163
|
"__superjsonMeta": {
|
|
164
164
|
"values": {
|
|
165
|
-
"paths./addBody.patch.responses.200.content.application/json.schema.items": [
|
|
165
|
+
"paths./base/addBody.patch.responses.200.content.application/json.schema.items": [
|
|
166
166
|
"undefined",
|
|
167
167
|
],
|
|
168
|
-
"paths./addBody.patch.responses.200.content.application/json.schema.patternProperties": [
|
|
168
|
+
"paths./base/addBody.patch.responses.200.content.application/json.schema.patternProperties": [
|
|
169
169
|
"undefined",
|
|
170
170
|
],
|
|
171
|
-
"paths./addBody.patch.responses.200.content.application/json.schema.required": [
|
|
171
|
+
"paths./base/addBody.patch.responses.200.content.application/json.schema.required": [
|
|
172
172
|
"undefined",
|
|
173
173
|
],
|
|
174
|
-
"paths./formWithSchemaForm.post.responses.200.content.multipart/form-data.schema.items": [
|
|
174
|
+
"paths./base/formWithSchemaForm.post.responses.200.content.multipart/form-data.schema.items": [
|
|
175
175
|
"undefined",
|
|
176
176
|
],
|
|
177
|
-
"paths./formWithSchemaForm.post.responses.200.content.multipart/form-data.schema.patternProperties": [
|
|
177
|
+
"paths./base/formWithSchemaForm.post.responses.200.content.multipart/form-data.schema.patternProperties": [
|
|
178
178
|
"undefined",
|
|
179
179
|
],
|
|
180
|
-
"paths./one/ids/{id}.get.parameters.0.description": [
|
|
180
|
+
"paths./base/one/ids/{id}.get.parameters.0.description": [
|
|
181
181
|
"undefined",
|
|
182
182
|
],
|
|
183
|
-
"paths./one/ids/{id}.get.parameters.0.examples": [
|
|
183
|
+
"paths./base/one/ids/{id}.get.parameters.0.examples": [
|
|
184
184
|
"undefined",
|
|
185
185
|
],
|
|
186
|
-
"paths./one/ids/{id}.get.responses.404.content.application/json.schema.items": [
|
|
186
|
+
"paths./base/one/ids/{id}.get.responses.404.content.application/json.schema.items": [
|
|
187
187
|
"undefined",
|
|
188
188
|
],
|
|
189
|
-
"paths./one/ids/{id}.get.responses.404.content.application/json.schema.patternProperties": [
|
|
189
|
+
"paths./base/one/ids/{id}.get.responses.404.content.application/json.schema.patternProperties": [
|
|
190
190
|
"undefined",
|
|
191
191
|
],
|
|
192
|
-
"paths./queryParams.get.parameters.0.description": [
|
|
192
|
+
"paths./base/queryParams.get.parameters.0.description": [
|
|
193
193
|
"undefined",
|
|
194
194
|
],
|
|
195
|
-
"paths./queryParams.get.parameters.0.examples": [
|
|
195
|
+
"paths./base/queryParams.get.parameters.0.examples": [
|
|
196
196
|
"undefined",
|
|
197
197
|
],
|
|
198
|
-
"paths./queryParams.get.responses.200.content.application/json.schema.items": [
|
|
198
|
+
"paths./base/queryParams.get.responses.200.content.application/json.schema.items": [
|
|
199
199
|
"undefined",
|
|
200
200
|
],
|
|
201
|
-
"paths./queryParams.get.responses.200.content.application/json.schema.patternProperties": [
|
|
201
|
+
"paths./base/queryParams.get.responses.200.content.application/json.schema.patternProperties": [
|
|
202
202
|
"undefined",
|
|
203
203
|
],
|
|
204
|
-
"paths./queryParams.get.responses.200.content.application/json.schema.required": [
|
|
204
|
+
"paths./base/queryParams.get.responses.200.content.application/json.schema.required": [
|
|
205
205
|
"undefined",
|
|
206
206
|
],
|
|
207
|
-
"paths./queryParams.post.responses.200.content.application/json.schema.items": [
|
|
207
|
+
"paths./base/queryParams.post.responses.200.content.application/json.schema.items": [
|
|
208
208
|
"undefined",
|
|
209
209
|
],
|
|
210
|
-
"paths./queryParams.post.responses.200.content.application/json.schema.patternProperties": [
|
|
210
|
+
"paths./base/queryParams.post.responses.200.content.application/json.schema.patternProperties": [
|
|
211
211
|
"undefined",
|
|
212
212
|
],
|
|
213
|
-
"paths./queryParams.post.responses.200.content.application/json.schema.required": [
|
|
213
|
+
"paths./base/queryParams.post.responses.200.content.application/json.schema.required": [
|
|
214
214
|
"undefined",
|
|
215
215
|
],
|
|
216
|
-
"paths./streamWithSchema.get.responses.200.content.application/json.schema.items": [
|
|
216
|
+
"paths./base/streamWithSchema.get.responses.200.content.application/json.schema.items": [
|
|
217
217
|
"undefined",
|
|
218
218
|
],
|
|
219
|
-
"paths./streamWithSchema.get.responses.200.content.application/json.schema.patternProperties": [
|
|
219
|
+
"paths./base/streamWithSchema.get.responses.200.content.application/json.schema.patternProperties": [
|
|
220
220
|
"undefined",
|
|
221
221
|
],
|
|
222
|
-
"paths./two/ids/{id}.get.parameters.0.description": [
|
|
222
|
+
"paths./base/two/ids/{id}.get.parameters.0.description": [
|
|
223
223
|
"undefined",
|
|
224
224
|
],
|
|
225
|
-
"paths./two/ids/{id}.get.parameters.0.examples": [
|
|
225
|
+
"paths./base/two/ids/{id}.get.parameters.0.examples": [
|
|
226
226
|
"undefined",
|
|
227
227
|
],
|
|
228
228
|
},
|
|
@@ -237,7 +237,7 @@ test('openapi response', async () => {
|
|
|
237
237
|
},
|
|
238
238
|
"openapi": "3.1.3",
|
|
239
239
|
"paths": {
|
|
240
|
-
"/addBody": {
|
|
240
|
+
"/base/addBody": {
|
|
241
241
|
"patch": {
|
|
242
242
|
"parameters": [],
|
|
243
243
|
"requestBody": {
|
|
@@ -288,7 +288,7 @@ test('openapi response', async () => {
|
|
|
288
288
|
},
|
|
289
289
|
},
|
|
290
290
|
},
|
|
291
|
-
"/formWithSchemaForm": {
|
|
291
|
+
"/base/formWithSchemaForm": {
|
|
292
292
|
"post": {
|
|
293
293
|
"description": "This returns form data with schema",
|
|
294
294
|
"responses": {
|
|
@@ -327,7 +327,7 @@ test('openapi response', async () => {
|
|
|
327
327
|
},
|
|
328
328
|
},
|
|
329
329
|
},
|
|
330
|
-
"/one/ids/{id}": {
|
|
330
|
+
"/base/one/ids/{id}": {
|
|
331
331
|
"get": {
|
|
332
332
|
"parameters": [
|
|
333
333
|
{
|
|
@@ -383,7 +383,7 @@ test('openapi response', async () => {
|
|
|
383
383
|
},
|
|
384
384
|
},
|
|
385
385
|
},
|
|
386
|
-
"/openapi": {
|
|
386
|
+
"/base/openapi": {
|
|
387
387
|
"get": {
|
|
388
388
|
"responses": {
|
|
389
389
|
"200": {
|
|
@@ -405,7 +405,7 @@ test('openapi response', async () => {
|
|
|
405
405
|
},
|
|
406
406
|
},
|
|
407
407
|
},
|
|
408
|
-
"/queryParams": {
|
|
408
|
+
"/base/queryParams": {
|
|
409
409
|
"get": {
|
|
410
410
|
"parameters": [
|
|
411
411
|
{
|
|
@@ -501,7 +501,7 @@ test('openapi response', async () => {
|
|
|
501
501
|
},
|
|
502
502
|
},
|
|
503
503
|
},
|
|
504
|
-
"/stream": {
|
|
504
|
+
"/base/stream": {
|
|
505
505
|
"get": {
|
|
506
506
|
"description": "This is a stream",
|
|
507
507
|
"responses": {
|
|
@@ -527,7 +527,7 @@ test('openapi response', async () => {
|
|
|
527
527
|
},
|
|
528
528
|
},
|
|
529
529
|
},
|
|
530
|
-
"/streamWithSchema": {
|
|
530
|
+
"/base/streamWithSchema": {
|
|
531
531
|
"get": {
|
|
532
532
|
"description": "This is a stream with schema",
|
|
533
533
|
"responses": {
|
|
@@ -565,7 +565,7 @@ test('openapi response', async () => {
|
|
|
565
565
|
},
|
|
566
566
|
},
|
|
567
567
|
},
|
|
568
|
-
"/two/ids/{id}": {
|
|
568
|
+
"/base/two/ids/{id}": {
|
|
569
569
|
"get": {
|
|
570
570
|
"parameters": [
|
|
571
571
|
{
|