@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,198 @@
|
|
|
1
|
+
import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
|
|
2
|
+
import OpenAPIClientAxios from 'openapi-client-axios'
|
|
3
|
+
import type { AxiosInstance } from 'axios'
|
|
4
|
+
import FormData from 'form-data'
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
import { Headers } from './polyfill-headers'
|
|
7
|
+
import { isFileUploadParameter } from '../openapi/file-upload'
|
|
8
|
+
|
|
9
|
+
export type HttpClientConfig = {
|
|
10
|
+
baseUrl: string
|
|
11
|
+
headers?: Record<string, string>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type HttpClientResponse<T = any> = {
|
|
15
|
+
data: T
|
|
16
|
+
status: number
|
|
17
|
+
headers: Headers
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class HttpClientError extends Error {
|
|
21
|
+
constructor(
|
|
22
|
+
message: string,
|
|
23
|
+
public status: number,
|
|
24
|
+
public data: any,
|
|
25
|
+
public headers?: Headers,
|
|
26
|
+
) {
|
|
27
|
+
super(`${status} ${message}`)
|
|
28
|
+
this.name = 'HttpClientError'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class HttpClient {
|
|
33
|
+
private api: Promise<AxiosInstance>
|
|
34
|
+
private client: OpenAPIClientAxios
|
|
35
|
+
|
|
36
|
+
constructor(config: HttpClientConfig, openApiSpec: OpenAPIV3.Document | OpenAPIV3_1.Document) {
|
|
37
|
+
// @ts-expect-error
|
|
38
|
+
this.client = new (OpenAPIClientAxios.default ?? OpenAPIClientAxios)({
|
|
39
|
+
definition: openApiSpec,
|
|
40
|
+
axiosConfigDefaults: {
|
|
41
|
+
baseURL: config.baseUrl,
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
'User-Agent': 'notion-mcp-server',
|
|
45
|
+
...config.headers,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
this.api = this.client.init()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private async prepareFileUpload(operation: OpenAPIV3.OperationObject, params: Record<string, any>): Promise<FormData | null> {
|
|
53
|
+
const fileParams = isFileUploadParameter(operation)
|
|
54
|
+
if (fileParams.length === 0) return null
|
|
55
|
+
|
|
56
|
+
const formData = new FormData()
|
|
57
|
+
|
|
58
|
+
// Handle file uploads
|
|
59
|
+
for (const param of fileParams) {
|
|
60
|
+
const filePath = params[param]
|
|
61
|
+
if (!filePath) {
|
|
62
|
+
throw new Error(`File path must be provided for parameter: ${param}`)
|
|
63
|
+
}
|
|
64
|
+
switch (typeof filePath) {
|
|
65
|
+
case 'string':
|
|
66
|
+
addFile(param, filePath)
|
|
67
|
+
break
|
|
68
|
+
case 'object':
|
|
69
|
+
if(Array.isArray(filePath)) {
|
|
70
|
+
let fileCount = 0
|
|
71
|
+
for(const file of filePath) {
|
|
72
|
+
addFile(param, file)
|
|
73
|
+
fileCount++
|
|
74
|
+
}
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
//deliberate fallthrough
|
|
78
|
+
default:
|
|
79
|
+
throw new Error(`Unsupported file type: ${typeof filePath}`)
|
|
80
|
+
}
|
|
81
|
+
function addFile(name: string, filePath: string) {
|
|
82
|
+
try {
|
|
83
|
+
const fileStream = fs.createReadStream(filePath)
|
|
84
|
+
formData.append(name, fileStream)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`Failed to read file at ${filePath}: ${error}`)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Add non-file parameters to form data
|
|
92
|
+
for (const [key, value] of Object.entries(params)) {
|
|
93
|
+
if (!fileParams.includes(key)) {
|
|
94
|
+
formData.append(key, value)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return formData
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Execute an OpenAPI operation
|
|
103
|
+
*/
|
|
104
|
+
async executeOperation<T = any>(
|
|
105
|
+
operation: OpenAPIV3.OperationObject & { method: string; path: string },
|
|
106
|
+
params: Record<string, any> = {},
|
|
107
|
+
): Promise<HttpClientResponse<T>> {
|
|
108
|
+
const api = await this.api
|
|
109
|
+
const operationId = operation.operationId
|
|
110
|
+
if (!operationId) {
|
|
111
|
+
throw new Error('Operation ID is required')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Handle file uploads if present
|
|
115
|
+
const formData = await this.prepareFileUpload(operation, params)
|
|
116
|
+
|
|
117
|
+
// Separate parameters based on their location
|
|
118
|
+
const urlParameters: Record<string, any> = {}
|
|
119
|
+
const bodyParams: Record<string, any> = formData || { ...params }
|
|
120
|
+
|
|
121
|
+
// Extract path and query parameters based on operation definition
|
|
122
|
+
if (operation.parameters) {
|
|
123
|
+
for (const param of operation.parameters) {
|
|
124
|
+
if ('name' in param && param.name && param.in) {
|
|
125
|
+
if (param.in === 'path' || param.in === 'query') {
|
|
126
|
+
if (params[param.name] !== undefined) {
|
|
127
|
+
urlParameters[param.name] = params[param.name]
|
|
128
|
+
if (!formData) {
|
|
129
|
+
delete bodyParams[param.name]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add all parameters as url parameters if there is no requestBody defined
|
|
138
|
+
if (!operation.requestBody && !formData) {
|
|
139
|
+
for (const key in bodyParams) {
|
|
140
|
+
if (bodyParams[key] !== undefined) {
|
|
141
|
+
urlParameters[key] = bodyParams[key]
|
|
142
|
+
delete bodyParams[key]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const operationFn = (api as any)[operationId]
|
|
148
|
+
if (!operationFn) {
|
|
149
|
+
throw new Error(`Operation ${operationId} not found`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
// If we have form data, we need to set the correct headers
|
|
154
|
+
const hasBody = Object.keys(bodyParams).length > 0
|
|
155
|
+
const headers = formData
|
|
156
|
+
? formData.getHeaders()
|
|
157
|
+
: { ...(hasBody ? { 'Content-Type': 'application/json' } : { 'Content-Type': null }) }
|
|
158
|
+
const requestConfig = {
|
|
159
|
+
headers: {
|
|
160
|
+
...headers,
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// first argument is url parameters, second is body parameters
|
|
165
|
+
const response = await operationFn(urlParameters, hasBody ? bodyParams : undefined, requestConfig)
|
|
166
|
+
|
|
167
|
+
// Convert axios headers to Headers object
|
|
168
|
+
const responseHeaders = new Headers()
|
|
169
|
+
Object.entries(response.headers).forEach(([key, value]) => {
|
|
170
|
+
if (value) responseHeaders.append(key, value.toString())
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
data: response.data,
|
|
175
|
+
status: response.status,
|
|
176
|
+
headers: responseHeaders,
|
|
177
|
+
}
|
|
178
|
+
} catch (error: any) {
|
|
179
|
+
if (error.response) {
|
|
180
|
+
// Only log errors in non-test environments to keep test output clean
|
|
181
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
182
|
+
console.error('Error in http client', {
|
|
183
|
+
status: error.response.status,
|
|
184
|
+
statusText: error.response.statusText,
|
|
185
|
+
data: error.response.data,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
const headers = new Headers()
|
|
189
|
+
Object.entries(error.response.headers).forEach(([key, value]) => {
|
|
190
|
+
if (value) headers.append(key, value.toString())
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
throw new HttpClientError(error.response.statusText || 'Request failed', error.response.status, error.response.data, headers)
|
|
194
|
+
}
|
|
195
|
+
throw error
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The Headers class was supported in Node.js starting with version 18, which was released on April 19, 2022.
|
|
3
|
+
* We need to have a polyfill ready to work for old Node versions.
|
|
4
|
+
* See more at https://github.com/makenotion/notion-mcp-server/issues/32
|
|
5
|
+
* */
|
|
6
|
+
class PolyfillHeaders {
|
|
7
|
+
private headers: Map<string, string[]> = new Map();
|
|
8
|
+
|
|
9
|
+
constructor(init?: Record<string, string>) {
|
|
10
|
+
if (init) {
|
|
11
|
+
Object.entries(init).forEach(([key, value]) => {
|
|
12
|
+
this.append(key, value);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public append(name: string, value: string): void {
|
|
18
|
+
const key = name.toLowerCase();
|
|
19
|
+
|
|
20
|
+
if (!this.headers.has(key)) {
|
|
21
|
+
this.headers.set(key, []);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.headers.get(key)!.push(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public get(name: string): string | null {
|
|
28
|
+
const key = name.toLowerCase();
|
|
29
|
+
|
|
30
|
+
if (!this.headers.has(key)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return this.headers.get(key)!.join(', ');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const GlobalHeaders = typeof global !== 'undefined' && 'Headers' in global
|
|
39
|
+
? (global as any).Headers
|
|
40
|
+
: undefined;
|
|
41
|
+
|
|
42
|
+
export const Headers = (GlobalHeaders || PolyfillHeaders);
|