@mieubrisse/notion-mcp-server 2.0.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/.devcontainer/devcontainer.json +4 -0
- package/.dockerignore +3 -0
- package/.github/pull_request_template.md +8 -0
- package/.github/workflows/ci.yml +42 -0
- package/Dockerfile +36 -0
- package/LICENSE +7 -0
- package/README.md +412 -0
- package/docker-compose.yml +6 -0
- package/docs/images/connections.png +0 -0
- package/docs/images/integration-access.png +0 -0
- package/docs/images/integrations-capabilities.png +0 -0
- package/docs/images/integrations-creation.png +0 -0
- package/docs/images/page-access-edit.png +0 -0
- package/package.json +63 -0
- package/scripts/build-cli.js +30 -0
- package/scripts/notion-openapi.json +2238 -0
- package/scripts/start-server.ts +243 -0
- package/src/init-server.ts +50 -0
- package/src/openapi-mcp-server/README.md +3 -0
- package/src/openapi-mcp-server/auth/index.ts +2 -0
- package/src/openapi-mcp-server/auth/template.ts +24 -0
- package/src/openapi-mcp-server/auth/types.ts +26 -0
- package/src/openapi-mcp-server/client/__tests__/http-client-upload.test.ts +205 -0
- package/src/openapi-mcp-server/client/__tests__/http-client.integration.test.ts +282 -0
- package/src/openapi-mcp-server/client/__tests__/http-client.test.ts +537 -0
- package/src/openapi-mcp-server/client/http-client.ts +198 -0
- package/src/openapi-mcp-server/client/polyfill-headers.ts +42 -0
- package/src/openapi-mcp-server/index.ts +3 -0
- package/src/openapi-mcp-server/mcp/__tests__/proxy.test.ts +479 -0
- package/src/openapi-mcp-server/mcp/proxy.ts +250 -0
- package/src/openapi-mcp-server/openapi/__tests__/file-upload.test.ts +100 -0
- package/src/openapi-mcp-server/openapi/__tests__/parser-multipart.test.ts +602 -0
- package/src/openapi-mcp-server/openapi/__tests__/parser.test.ts +1448 -0
- package/src/openapi-mcp-server/openapi/file-upload.ts +40 -0
- package/src/openapi-mcp-server/openapi/parser.ts +529 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Identifies file upload parameters in an OpenAPI operation
|
|
5
|
+
* @param operation The OpenAPI operation object to check
|
|
6
|
+
* @returns Array of parameter names that are file uploads
|
|
7
|
+
*/
|
|
8
|
+
export function isFileUploadParameter(operation: OpenAPIV3.OperationObject): string[] {
|
|
9
|
+
const fileParams: string[] = []
|
|
10
|
+
|
|
11
|
+
if (!operation.requestBody) return fileParams
|
|
12
|
+
|
|
13
|
+
const requestBody = operation.requestBody as OpenAPIV3.RequestBodyObject
|
|
14
|
+
const content = requestBody.content || {}
|
|
15
|
+
|
|
16
|
+
// Check multipart/form-data content type for file uploads
|
|
17
|
+
const multipartContent = content['multipart/form-data']
|
|
18
|
+
if (!multipartContent?.schema) return fileParams
|
|
19
|
+
|
|
20
|
+
const schema = multipartContent.schema as OpenAPIV3.SchemaObject
|
|
21
|
+
if (schema.type !== 'object' || !schema.properties) return fileParams
|
|
22
|
+
|
|
23
|
+
// Look for properties with type: string, format: binary which indicates file uploads
|
|
24
|
+
Object.entries(schema.properties).forEach(([propName, prop]) => {
|
|
25
|
+
const schemaProp = prop as OpenAPIV3.SchemaObject
|
|
26
|
+
if (schemaProp.type === 'string' && schemaProp.format === 'binary') {
|
|
27
|
+
fileParams.push(propName)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check for array of files
|
|
31
|
+
if (schemaProp.type === 'array' && schemaProp.items) {
|
|
32
|
+
const itemSchema = schemaProp.items as OpenAPIV3.SchemaObject
|
|
33
|
+
if (itemSchema.type === 'string' && itemSchema.format === 'binary') {
|
|
34
|
+
fileParams.push(propName)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return fileParams
|
|
40
|
+
}
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
|
|
2
|
+
import type { JSONSchema7 as IJsonSchema } from 'json-schema'
|
|
3
|
+
import type { ChatCompletionTool } from 'openai/resources/chat/completions'
|
|
4
|
+
import type { Tool } from '@anthropic-ai/sdk/resources/messages/messages'
|
|
5
|
+
|
|
6
|
+
type NewToolMethod = {
|
|
7
|
+
name: string
|
|
8
|
+
description: string
|
|
9
|
+
inputSchema: IJsonSchema & { type: 'object' }
|
|
10
|
+
returnSchema?: IJsonSchema
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type FunctionParameters = {
|
|
14
|
+
type: 'object'
|
|
15
|
+
properties?: Record<string, unknown>
|
|
16
|
+
required?: string[]
|
|
17
|
+
[key: string]: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class OpenAPIToMCPConverter {
|
|
21
|
+
private schemaCache: Record<string, IJsonSchema> = {}
|
|
22
|
+
private nameCounter: number = 0
|
|
23
|
+
|
|
24
|
+
constructor(private openApiSpec: OpenAPIV3.Document | OpenAPIV3_1.Document) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a $ref reference to its schema in the openApiSpec.
|
|
28
|
+
* Returns the raw OpenAPI SchemaObject or null if not found.
|
|
29
|
+
*/
|
|
30
|
+
private internalResolveRef(ref: string, resolvedRefs: Set<string>): OpenAPIV3.SchemaObject | null {
|
|
31
|
+
if (!ref.startsWith('#/')) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
if (resolvedRefs.has(ref)) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parts = ref.replace(/^#\//, '').split('/')
|
|
39
|
+
let current: any = this.openApiSpec
|
|
40
|
+
for (const part of parts) {
|
|
41
|
+
current = current[part]
|
|
42
|
+
if (!current) return null
|
|
43
|
+
}
|
|
44
|
+
resolvedRefs.add(ref)
|
|
45
|
+
return current as OpenAPIV3.SchemaObject
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert an OpenAPI schema (or reference) into a JSON Schema object.
|
|
50
|
+
* Uses caching and handles cycles by returning $ref nodes.
|
|
51
|
+
*/
|
|
52
|
+
convertOpenApiSchemaToJsonSchema(
|
|
53
|
+
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
|
|
54
|
+
resolvedRefs: Set<string>,
|
|
55
|
+
resolveRefs: boolean = false,
|
|
56
|
+
): IJsonSchema {
|
|
57
|
+
if ('$ref' in schema) {
|
|
58
|
+
const ref = schema.$ref
|
|
59
|
+
if (!resolveRefs) {
|
|
60
|
+
if (ref.startsWith('#/components/schemas/')) {
|
|
61
|
+
return {
|
|
62
|
+
$ref: ref.replace(/^#\/components\/schemas\//, '#/$defs/'),
|
|
63
|
+
...('description' in schema ? { description: schema.description as string } : {}),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
console.error(`Attempting to resolve ref ${ref} not found in components collection.`)
|
|
67
|
+
// deliberate fall through
|
|
68
|
+
}
|
|
69
|
+
// Create base schema with $ref and description if present
|
|
70
|
+
const refSchema: IJsonSchema = { $ref: ref }
|
|
71
|
+
if ('description' in schema && schema.description) {
|
|
72
|
+
refSchema.description = schema.description as string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If already cached, return immediately with description
|
|
76
|
+
if (this.schemaCache[ref]) {
|
|
77
|
+
return this.schemaCache[ref]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const resolved = this.internalResolveRef(ref, resolvedRefs)
|
|
81
|
+
if (!resolved) {
|
|
82
|
+
// TODO: need extensive tests for this and we definitely need to handle the case of self references
|
|
83
|
+
console.error(`Failed to resolve ref ${ref}`)
|
|
84
|
+
return {
|
|
85
|
+
$ref: ref.replace(/^#\/components\/schemas\//, '#/$defs/'),
|
|
86
|
+
description: 'description' in schema ? ((schema.description as string) ?? '') : '',
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
const converted = this.convertOpenApiSchemaToJsonSchema(resolved, resolvedRefs, resolveRefs)
|
|
90
|
+
this.schemaCache[ref] = converted
|
|
91
|
+
|
|
92
|
+
return converted
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Handle inline schema
|
|
97
|
+
const result: IJsonSchema = {}
|
|
98
|
+
|
|
99
|
+
if (schema.type) {
|
|
100
|
+
result.type = schema.type as IJsonSchema['type']
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Convert binary format to uri-reference and enhance description
|
|
104
|
+
if (schema.format === 'binary') {
|
|
105
|
+
result.format = 'uri-reference'
|
|
106
|
+
const binaryDesc = 'absolute paths to local files'
|
|
107
|
+
result.description = schema.description ? `${schema.description} (${binaryDesc})` : binaryDesc
|
|
108
|
+
} else {
|
|
109
|
+
if (schema.format) {
|
|
110
|
+
result.format = schema.format
|
|
111
|
+
}
|
|
112
|
+
if (schema.description) {
|
|
113
|
+
result.description = schema.description
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (schema.enum) {
|
|
118
|
+
result.enum = schema.enum
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (schema.default !== undefined) {
|
|
122
|
+
result.default = schema.default
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle object properties
|
|
126
|
+
if (schema.type === 'object') {
|
|
127
|
+
result.type = 'object'
|
|
128
|
+
if (schema.properties) {
|
|
129
|
+
result.properties = {}
|
|
130
|
+
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
131
|
+
result.properties[name] = this.convertOpenApiSchemaToJsonSchema(propSchema, resolvedRefs, resolveRefs)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (schema.required) {
|
|
135
|
+
result.required = schema.required
|
|
136
|
+
}
|
|
137
|
+
if (schema.additionalProperties === true || schema.additionalProperties === undefined) {
|
|
138
|
+
result.additionalProperties = true
|
|
139
|
+
} else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
140
|
+
result.additionalProperties = this.convertOpenApiSchemaToJsonSchema(schema.additionalProperties, resolvedRefs, resolveRefs)
|
|
141
|
+
} else {
|
|
142
|
+
result.additionalProperties = false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Handle arrays - ensure binary format conversion happens for array items too
|
|
147
|
+
if (schema.type === 'array' && schema.items) {
|
|
148
|
+
result.type = 'array'
|
|
149
|
+
result.items = this.convertOpenApiSchemaToJsonSchema(schema.items, resolvedRefs, resolveRefs)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// oneOf, anyOf, allOf
|
|
153
|
+
if (schema.oneOf) {
|
|
154
|
+
result.oneOf = schema.oneOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs))
|
|
155
|
+
}
|
|
156
|
+
if (schema.anyOf) {
|
|
157
|
+
result.anyOf = schema.anyOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs))
|
|
158
|
+
}
|
|
159
|
+
if (schema.allOf) {
|
|
160
|
+
result.allOf = schema.allOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
convertToMCPTools(): {
|
|
167
|
+
tools: Record<string, { methods: NewToolMethod[] }>
|
|
168
|
+
openApiLookup: Record<string, OpenAPIV3.OperationObject & { method: string; path: string }>
|
|
169
|
+
zip: Record<string, { openApi: OpenAPIV3.OperationObject & { method: string; path: string }; mcp: NewToolMethod }>
|
|
170
|
+
} {
|
|
171
|
+
const apiName = 'API'
|
|
172
|
+
|
|
173
|
+
const openApiLookup: Record<string, OpenAPIV3.OperationObject & { method: string; path: string }> = {}
|
|
174
|
+
const tools: Record<string, { methods: NewToolMethod[] }> = {
|
|
175
|
+
[apiName]: { methods: [] },
|
|
176
|
+
}
|
|
177
|
+
const zip: Record<string, { openApi: OpenAPIV3.OperationObject & { method: string; path: string }; mcp: NewToolMethod }> = {}
|
|
178
|
+
for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) {
|
|
179
|
+
if (!pathItem) continue
|
|
180
|
+
|
|
181
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
182
|
+
if (!this.isOperation(method, operation)) continue
|
|
183
|
+
|
|
184
|
+
const mcpMethod = this.convertOperationToMCPMethod(operation, method, path)
|
|
185
|
+
if (mcpMethod) {
|
|
186
|
+
const uniqueName = this.ensureUniqueName(mcpMethod.name)
|
|
187
|
+
mcpMethod.name = uniqueName
|
|
188
|
+
// Apply description prefix to the already-built description (which includes error responses)
|
|
189
|
+
mcpMethod.description = this.getDescription(mcpMethod.description)
|
|
190
|
+
tools[apiName]!.methods.push(mcpMethod)
|
|
191
|
+
openApiLookup[apiName + '-' + uniqueName] = { ...operation, method, path }
|
|
192
|
+
zip[apiName + '-' + uniqueName] = { openApi: { ...operation, method, path }, mcp: mcpMethod }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { tools, openApiLookup, zip }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Convert the OpenAPI spec to OpenAI's ChatCompletionTool format
|
|
202
|
+
*/
|
|
203
|
+
convertToOpenAITools(): ChatCompletionTool[] {
|
|
204
|
+
const tools: ChatCompletionTool[] = []
|
|
205
|
+
|
|
206
|
+
for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) {
|
|
207
|
+
if (!pathItem) continue
|
|
208
|
+
|
|
209
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
210
|
+
if (!this.isOperation(method, operation)) continue
|
|
211
|
+
|
|
212
|
+
const parameters = this.convertOperationToJsonSchema(operation, method, path)
|
|
213
|
+
const tool: ChatCompletionTool = {
|
|
214
|
+
type: 'function',
|
|
215
|
+
function: {
|
|
216
|
+
name: operation.operationId!,
|
|
217
|
+
description: this.getDescription(operation.summary || operation.description || ''),
|
|
218
|
+
parameters: parameters as FunctionParameters,
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
tools.push(tool)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return tools
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Convert the OpenAPI spec to Anthropic's Tool format
|
|
230
|
+
*/
|
|
231
|
+
convertToAnthropicTools(): Tool[] {
|
|
232
|
+
const tools: Tool[] = []
|
|
233
|
+
|
|
234
|
+
for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) {
|
|
235
|
+
if (!pathItem) continue
|
|
236
|
+
|
|
237
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
238
|
+
if (!this.isOperation(method, operation)) continue
|
|
239
|
+
|
|
240
|
+
const parameters = this.convertOperationToJsonSchema(operation, method, path)
|
|
241
|
+
const tool: Tool = {
|
|
242
|
+
name: operation.operationId!,
|
|
243
|
+
description: this.getDescription(operation.summary || operation.description || ''),
|
|
244
|
+
input_schema: parameters as Tool['input_schema'],
|
|
245
|
+
}
|
|
246
|
+
tools.push(tool)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return tools
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private convertComponentsToJsonSchema(): Record<string, IJsonSchema> {
|
|
254
|
+
const components = this.openApiSpec.components || {}
|
|
255
|
+
const schema: Record<string, IJsonSchema> = {}
|
|
256
|
+
for (const [key, value] of Object.entries(components.schemas || {})) {
|
|
257
|
+
schema[key] = this.convertOpenApiSchemaToJsonSchema(value, new Set())
|
|
258
|
+
}
|
|
259
|
+
return schema
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Helper method to convert an operation to a JSON Schema for parameters
|
|
263
|
+
*/
|
|
264
|
+
private convertOperationToJsonSchema(
|
|
265
|
+
operation: OpenAPIV3.OperationObject,
|
|
266
|
+
method: string,
|
|
267
|
+
path: string,
|
|
268
|
+
): IJsonSchema & { type: 'object' } {
|
|
269
|
+
const schema: IJsonSchema & { type: 'object' } = {
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: {},
|
|
272
|
+
required: [],
|
|
273
|
+
$defs: this.convertComponentsToJsonSchema(),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Handle parameters (path, query, header, cookie)
|
|
277
|
+
if (operation.parameters) {
|
|
278
|
+
for (const param of operation.parameters) {
|
|
279
|
+
const paramObj = this.resolveParameter(param)
|
|
280
|
+
if (paramObj && paramObj.schema) {
|
|
281
|
+
const paramSchema = this.convertOpenApiSchemaToJsonSchema(paramObj.schema, new Set())
|
|
282
|
+
// Merge parameter-level description if available
|
|
283
|
+
if (paramObj.description) {
|
|
284
|
+
paramSchema.description = paramObj.description
|
|
285
|
+
}
|
|
286
|
+
schema.properties![paramObj.name] = paramSchema
|
|
287
|
+
if (paramObj.required) {
|
|
288
|
+
schema.required!.push(paramObj.name)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Handle requestBody
|
|
295
|
+
if (operation.requestBody) {
|
|
296
|
+
const bodyObj = this.resolveRequestBody(operation.requestBody)
|
|
297
|
+
if (bodyObj?.content) {
|
|
298
|
+
if (bodyObj.content['application/json']?.schema) {
|
|
299
|
+
const bodySchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['application/json'].schema, new Set())
|
|
300
|
+
if (bodySchema.type === 'object' && bodySchema.properties) {
|
|
301
|
+
for (const [name, propSchema] of Object.entries(bodySchema.properties)) {
|
|
302
|
+
schema.properties![name] = propSchema
|
|
303
|
+
}
|
|
304
|
+
if (bodySchema.required) {
|
|
305
|
+
schema.required!.push(...bodySchema.required)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return schema
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private isOperation(method: string, operation: any): operation is OpenAPIV3.OperationObject {
|
|
316
|
+
return ['get', 'post', 'put', 'delete', 'patch'].includes(method.toLowerCase())
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private isParameterObject(param: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject): param is OpenAPIV3.ParameterObject {
|
|
320
|
+
return !('$ref' in param)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private isRequestBodyObject(body: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject): body is OpenAPIV3.RequestBodyObject {
|
|
324
|
+
return !('$ref' in body)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private resolveParameter(param: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject): OpenAPIV3.ParameterObject | null {
|
|
328
|
+
if (this.isParameterObject(param)) {
|
|
329
|
+
return param
|
|
330
|
+
} else {
|
|
331
|
+
const resolved = this.internalResolveRef(param.$ref, new Set())
|
|
332
|
+
if (resolved && (resolved as OpenAPIV3.ParameterObject).name) {
|
|
333
|
+
return resolved as OpenAPIV3.ParameterObject
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return null
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private resolveRequestBody(body: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject): OpenAPIV3.RequestBodyObject | null {
|
|
340
|
+
if (this.isRequestBodyObject(body)) {
|
|
341
|
+
return body
|
|
342
|
+
} else {
|
|
343
|
+
const resolved = this.internalResolveRef(body.$ref, new Set())
|
|
344
|
+
if (resolved) {
|
|
345
|
+
return resolved as OpenAPIV3.RequestBodyObject
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return null
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private resolveResponse(response: OpenAPIV3.ResponseObject | OpenAPIV3.ReferenceObject): OpenAPIV3.ResponseObject | null {
|
|
352
|
+
if ('$ref' in response) {
|
|
353
|
+
const resolved = this.internalResolveRef(response.$ref, new Set())
|
|
354
|
+
if (resolved) {
|
|
355
|
+
return resolved as OpenAPIV3.ResponseObject
|
|
356
|
+
} else {
|
|
357
|
+
return null
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return response
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private convertOperationToMCPMethod(operation: OpenAPIV3.OperationObject, method: string, path: string): NewToolMethod | null {
|
|
364
|
+
if (!operation.operationId) {
|
|
365
|
+
console.warn(`Operation without operationId at ${method} ${path}`)
|
|
366
|
+
return null
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const methodName = operation.operationId
|
|
370
|
+
|
|
371
|
+
const inputSchema: IJsonSchema & { type: 'object' } = {
|
|
372
|
+
$defs: this.convertComponentsToJsonSchema(),
|
|
373
|
+
type: 'object',
|
|
374
|
+
properties: {},
|
|
375
|
+
required: [],
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Handle parameters (path, query, header, cookie)
|
|
379
|
+
if (operation.parameters) {
|
|
380
|
+
for (const param of operation.parameters) {
|
|
381
|
+
const paramObj = this.resolveParameter(param)
|
|
382
|
+
if (paramObj && paramObj.schema) {
|
|
383
|
+
const schema = this.convertOpenApiSchemaToJsonSchema(paramObj.schema, new Set(), false)
|
|
384
|
+
// Merge parameter-level description if available
|
|
385
|
+
if (paramObj.description) {
|
|
386
|
+
schema.description = paramObj.description
|
|
387
|
+
}
|
|
388
|
+
inputSchema.properties![paramObj.name] = schema
|
|
389
|
+
if (paramObj.required) {
|
|
390
|
+
inputSchema.required!.push(paramObj.name)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Handle requestBody
|
|
397
|
+
if (operation.requestBody) {
|
|
398
|
+
const bodyObj = this.resolveRequestBody(operation.requestBody)
|
|
399
|
+
if (bodyObj?.content) {
|
|
400
|
+
// Handle multipart/form-data for file uploads
|
|
401
|
+
// We convert the multipart/form-data schema to a JSON schema and we require
|
|
402
|
+
// that the user passes in a string for each file that points to the local file
|
|
403
|
+
if (bodyObj.content['multipart/form-data']?.schema) {
|
|
404
|
+
const formSchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['multipart/form-data'].schema, new Set(), false)
|
|
405
|
+
if (formSchema.type === 'object' && formSchema.properties) {
|
|
406
|
+
for (const [name, propSchema] of Object.entries(formSchema.properties)) {
|
|
407
|
+
inputSchema.properties![name] = propSchema
|
|
408
|
+
}
|
|
409
|
+
if (formSchema.required) {
|
|
410
|
+
inputSchema.required!.push(...formSchema.required!)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Handle application/json
|
|
415
|
+
else if (bodyObj.content['application/json']?.schema) {
|
|
416
|
+
const bodySchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['application/json'].schema, new Set(), false)
|
|
417
|
+
// Merge body schema into the inputSchema's properties
|
|
418
|
+
if (bodySchema.type === 'object' && bodySchema.properties) {
|
|
419
|
+
for (const [name, propSchema] of Object.entries(bodySchema.properties)) {
|
|
420
|
+
inputSchema.properties![name] = propSchema
|
|
421
|
+
}
|
|
422
|
+
if (bodySchema.required) {
|
|
423
|
+
inputSchema.required!.push(...bodySchema.required!)
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
// If the request body is not an object, just put it under "body"
|
|
427
|
+
inputSchema.properties!['body'] = bodySchema
|
|
428
|
+
inputSchema.required!.push('body')
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Build description including error responses
|
|
435
|
+
let description = operation.summary || operation.description || ''
|
|
436
|
+
if (operation.responses) {
|
|
437
|
+
const errorResponses = Object.entries(operation.responses)
|
|
438
|
+
.filter(([code]) => code.startsWith('4') || code.startsWith('5'))
|
|
439
|
+
.map(([code, response]) => {
|
|
440
|
+
const responseObj = this.resolveResponse(response)
|
|
441
|
+
let errorDesc = responseObj?.description || ''
|
|
442
|
+
return `${code}: ${errorDesc}`
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
if (errorResponses.length > 0) {
|
|
446
|
+
description += '\nError Responses:\n' + errorResponses.join('\n')
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Extract return type (response schema)
|
|
451
|
+
const returnSchema = this.extractResponseType(operation.responses)
|
|
452
|
+
|
|
453
|
+
// Generate Zod schema from input schema
|
|
454
|
+
try {
|
|
455
|
+
// const zodSchemaStr = jsonSchemaToZod(inputSchema, { module: "cjs" })
|
|
456
|
+
// console.log(zodSchemaStr)
|
|
457
|
+
// // Execute the function with the zod instance
|
|
458
|
+
// const zodSchema = eval(zodSchemaStr) as z.ZodType
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
name: methodName,
|
|
462
|
+
description,
|
|
463
|
+
inputSchema,
|
|
464
|
+
...(returnSchema ? { returnSchema } : {}),
|
|
465
|
+
}
|
|
466
|
+
} catch (error) {
|
|
467
|
+
console.warn(`Failed to generate Zod schema for ${methodName}:`, error)
|
|
468
|
+
// Fallback to a basic object schema
|
|
469
|
+
return {
|
|
470
|
+
name: methodName,
|
|
471
|
+
description,
|
|
472
|
+
inputSchema,
|
|
473
|
+
...(returnSchema ? { returnSchema } : {}),
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private extractResponseType(responses: OpenAPIV3.ResponsesObject | undefined): IJsonSchema | null {
|
|
479
|
+
// Look for a success response
|
|
480
|
+
const successResponse = responses?.['200'] || responses?.['201'] || responses?.['202'] || responses?.['204']
|
|
481
|
+
if (!successResponse) return null
|
|
482
|
+
|
|
483
|
+
const responseObj = this.resolveResponse(successResponse)
|
|
484
|
+
if (!responseObj || !responseObj.content) return null
|
|
485
|
+
|
|
486
|
+
if (responseObj.content['application/json']?.schema) {
|
|
487
|
+
const returnSchema = this.convertOpenApiSchemaToJsonSchema(responseObj.content['application/json'].schema, new Set(), false)
|
|
488
|
+
returnSchema['$defs'] = this.convertComponentsToJsonSchema()
|
|
489
|
+
|
|
490
|
+
// Preserve the response description if available and not already set
|
|
491
|
+
if (responseObj.description && !returnSchema.description) {
|
|
492
|
+
returnSchema.description = responseObj.description
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return returnSchema
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// If no JSON response, fallback to a generic string or known formats
|
|
499
|
+
if (responseObj.content['image/png'] || responseObj.content['image/jpeg']) {
|
|
500
|
+
return { type: 'string', format: 'binary', description: responseObj.description || '' }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Fallback
|
|
504
|
+
return { type: 'string', description: responseObj.description || '' }
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private ensureUniqueName(name: string): string {
|
|
508
|
+
if (name.length <= 64) {
|
|
509
|
+
return name
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const truncatedName = name.slice(0, 64 - 5) // Reserve space for suffix
|
|
513
|
+
const uniqueSuffix = this.generateUniqueSuffix()
|
|
514
|
+
return `${truncatedName}-${uniqueSuffix}`
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private generateUniqueSuffix(): string {
|
|
518
|
+
this.nameCounter += 1
|
|
519
|
+
return this.nameCounter.toString().padStart(4, '0')
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private getDescription(description: string): string {
|
|
523
|
+
// Only add "Notion | " prefix for the Notion API
|
|
524
|
+
if (this.openApiSpec.info.title === 'Notion API') {
|
|
525
|
+
return "Notion | " + description
|
|
526
|
+
}
|
|
527
|
+
return description
|
|
528
|
+
}
|
|
529
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"composite": true,
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"declarationMap": true,
|
|
6
|
+
"sourceMap": true,
|
|
7
|
+
"outDir": "./build",
|
|
8
|
+
"target": "es2021",
|
|
9
|
+
"lib": ["es2022"],
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"module": "es2022",
|
|
12
|
+
"moduleResolution": "Bundler",
|
|
13
|
+
"types": [
|
|
14
|
+
"node"
|
|
15
|
+
],
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"allowJs": true,
|
|
18
|
+
"checkJs": false,
|
|
19
|
+
"isolatedModules": true,
|
|
20
|
+
"allowSyntheticDefaultImports": true,
|
|
21
|
+
"forceConsistentCasingInFileNames": true,
|
|
22
|
+
"strict": true,
|
|
23
|
+
"skipLibCheck": true
|
|
24
|
+
},
|
|
25
|
+
"include": [ "test/**/*.ts", "scripts/**/*.ts", "src/**/*.ts"]
|
|
26
|
+
}
|