@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.
Files changed (36) hide show
  1. package/.devcontainer/devcontainer.json +4 -0
  2. package/.dockerignore +3 -0
  3. package/.github/pull_request_template.md +8 -0
  4. package/.github/workflows/ci.yml +42 -0
  5. package/Dockerfile +36 -0
  6. package/LICENSE +7 -0
  7. package/README.md +412 -0
  8. package/docker-compose.yml +6 -0
  9. package/docs/images/connections.png +0 -0
  10. package/docs/images/integration-access.png +0 -0
  11. package/docs/images/integrations-capabilities.png +0 -0
  12. package/docs/images/integrations-creation.png +0 -0
  13. package/docs/images/page-access-edit.png +0 -0
  14. package/package.json +63 -0
  15. package/scripts/build-cli.js +30 -0
  16. package/scripts/notion-openapi.json +2238 -0
  17. package/scripts/start-server.ts +243 -0
  18. package/src/init-server.ts +50 -0
  19. package/src/openapi-mcp-server/README.md +3 -0
  20. package/src/openapi-mcp-server/auth/index.ts +2 -0
  21. package/src/openapi-mcp-server/auth/template.ts +24 -0
  22. package/src/openapi-mcp-server/auth/types.ts +26 -0
  23. package/src/openapi-mcp-server/client/__tests__/http-client-upload.test.ts +205 -0
  24. package/src/openapi-mcp-server/client/__tests__/http-client.integration.test.ts +282 -0
  25. package/src/openapi-mcp-server/client/__tests__/http-client.test.ts +537 -0
  26. package/src/openapi-mcp-server/client/http-client.ts +198 -0
  27. package/src/openapi-mcp-server/client/polyfill-headers.ts +42 -0
  28. package/src/openapi-mcp-server/index.ts +3 -0
  29. package/src/openapi-mcp-server/mcp/__tests__/proxy.test.ts +479 -0
  30. package/src/openapi-mcp-server/mcp/proxy.ts +250 -0
  31. package/src/openapi-mcp-server/openapi/__tests__/file-upload.test.ts +100 -0
  32. package/src/openapi-mcp-server/openapi/__tests__/parser-multipart.test.ts +602 -0
  33. package/src/openapi-mcp-server/openapi/__tests__/parser.test.ts +1448 -0
  34. package/src/openapi-mcp-server/openapi/file-upload.ts +40 -0
  35. package/src/openapi-mcp-server/openapi/parser.ts +529 -0
  36. 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);
@@ -0,0 +1,3 @@
1
+ export { OpenAPIToMCPConverter } from './openapi/parser'
2
+ export { HttpClient } from './client/http-client'
3
+ export type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'