-
Notifications
You must be signed in to change notification settings - Fork 625
Expand file tree
/
Copy pathserver-options.ts
More file actions
220 lines (195 loc) · 8.22 KB
/
Copy pathserver-options.ts
File metadata and controls
220 lines (195 loc) · 8.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import type { StreamableHTTPServerTransportOptions } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
export const DEFAULT_HTTP_HOST = '127.0.0.1'
export type ServerOptions = {
transport: string
port: number
host: string
authToken: string | undefined
unsafeDisableAuth: boolean
usedDeprecatedDisableAuthFlag: boolean
enableTokenPassthrough: boolean
enableStatelessHttp: boolean
allowedHosts: string[]
}
type DnsRebindingProtectionOptions = Pick<
StreamableHTTPServerTransportOptions,
'allowedHosts' | 'allowedOrigins' | 'enableDnsRebindingProtection'
>
export function parseServerOptions(argv: string[] = process.argv): ServerOptions {
const args = argv.slice(2)
let transport = 'stdio'
let port = 3000
let host = DEFAULT_HTTP_HOST
let authToken: string | undefined
let unsafeDisableAuth = false
let usedDeprecatedDisableAuthFlag = false
let enableTokenPassthrough = process.env.ENABLE_TOKEN_PASSTHROUGH === 'true'
let enableStatelessHttp = process.env.ENABLE_STATELESS_HTTP === 'true'
let allowedHosts: string[] = []
for (let i = 0; i < args.length; i++) {
if (args[i] === '--transport' && i + 1 < args.length) {
transport = args[i + 1]
i++
} else if (args[i] === '--port' && i + 1 < args.length) {
port = parseInt(args[i + 1], 10)
i++
} else if (args[i] === '--host' && i + 1 < args.length) {
host = args[i + 1]
i++
} else if (args[i] === '--auth-token' && i + 1 < args.length) {
authToken = args[i + 1]
i++
} else if (args[i] === '--unsafe-disable-auth') {
unsafeDisableAuth = true
} else if (args[i] === '--disable-auth') {
unsafeDisableAuth = true
usedDeprecatedDisableAuthFlag = true
} else if (args[i] === '--enable-token-passthrough') {
enableTokenPassthrough = true
} else if (args[i] === '--stateless-http') {
enableStatelessHttp = true
} else if (args[i] === '--allowed-hosts' && i + 1 < args.length) {
allowedHosts.push(
...args[i + 1]
.split(',')
.map((host) => host.trim())
.filter(Boolean),
)
i++
} else if (args[i] === '--help' || args[i] === '-h') {
console.log(getHelpText())
process.exit(0)
}
// Ignore unrecognized arguments (like command name passed by Docker)
}
return {
transport: transport.toLowerCase(),
port,
host,
authToken,
unsafeDisableAuth,
usedDeprecatedDisableAuthFlag,
enableTokenPassthrough,
enableStatelessHttp,
allowedHosts,
}
}
export function getHelpText(): string {
return `
Usage: notion-mcp-server [options]
Options:
--transport <type> Transport type: 'stdio' or 'http' (default: stdio)
--port <number> Port for HTTP server when using Streamable HTTP transport (default: 3000)
--host <host> Host for HTTP server when using Streamable HTTP transport (default: 127.0.0.1)
--auth-token <token> Bearer token for HTTP transport authentication (auto-generated if not provided)
--unsafe-disable-auth Disable bearer token authentication for HTTP transport. Unsafe; use only on isolated networks.
--disable-auth Deprecated alias for --unsafe-disable-auth
--allowed-hosts <hosts> Extra comma-separated hosts allowed by DNS rebinding protection when auth is disabled
--enable-token-passthrough Let each HTTP client supply its own Notion token per request
via the 'Notion-Token' header, so one deployment can serve
multiple Notion integrations (default: off).
--stateless-http Disable MCP session tracking for HTTP transport. Each POST request
gets a fresh transport, and GET/DELETE /mcp are rejected (default: off).
--help, -h Show this help message
Environment Variables:
NOTION_TOKEN Notion integration token (recommended)
OPENAPI_MCP_HEADERS JSON string with Notion API headers (alternative)
AUTH_TOKEN Bearer token for HTTP transport authentication (alternative to --auth-token)
ENABLE_TOKEN_PASSTHROUGH Set to 'true' to enable per-request Notion tokens (alternative to --enable-token-passthrough)
ENABLE_STATELESS_HTTP Set to 'true' to disable HTTP session tracking (alternative to --stateless-http)
Examples:
notion-mcp-server # Use stdio transport (default)
notion-mcp-server --transport stdio # Use stdio transport explicitly
notion-mcp-server --transport http # Use Streamable HTTP transport on 127.0.0.1:3000
notion-mcp-server --transport http --port 8080 # Use Streamable HTTP transport on port 8080
notion-mcp-server --transport http --host 0.0.0.0 # Bind HTTP transport to all interfaces
notion-mcp-server --transport http --auth-token mytoken # Use Streamable HTTP transport with custom auth token
notion-mcp-server --transport http --unsafe-disable-auth # Use Streamable HTTP transport without authentication
AUTH_TOKEN=mytoken notion-mcp-server --transport http # Use Streamable HTTP transport with auth token from env var
notion-mcp-server --transport http --enable-token-passthrough # Per-request Notion token via the Notion-Token header
notion-mcp-server --transport http --stateless-http # Stateless Streamable HTTP transport
`
}
export function getUnsafeAuthWarnings(options: ServerOptions): string[] {
if (!options.unsafeDisableAuth) {
return []
}
const warnings = [
'WARNING: --unsafe-disable-auth disables bearer token authentication. A malicious website may be able to reach this server via DNS rebinding. Only use this on an isolated network.',
]
if (options.usedDeprecatedDisableAuthFlag) {
warnings.unshift(
'WARNING: --disable-auth is deprecated because it is unsafe. Use --unsafe-disable-auth if you intentionally need unauthenticated HTTP.',
)
}
if (!isLoopbackHost(options.host)) {
warnings.push(
`WARNING: unauthenticated HTTP is bound to ${options.host}. Prefer the default ${DEFAULT_HTTP_HOST} loopback binding unless this is an isolated network.`,
)
}
return warnings
}
export function getDnsRebindingProtectionOptions(
options: ServerOptions,
): DnsRebindingProtectionOptions | undefined {
if (options.transport !== 'http' || !options.unsafeDisableAuth) {
return undefined
}
return {
enableDnsRebindingProtection: true,
allowedHosts: getAllowedHosts(options.host, options.port, options.allowedHosts),
allowedOrigins: getAllowedOrigins(options.host, options.port, options.allowedHosts),
}
}
export function getHttpServerDisplayUrl(options: ServerOptions): string {
return `http://${formatHostForUrl(displayHostForBinding(options.host))}:${options.port}`
}
function getAllowedHosts(host: string, port: number, extraHosts: string[]): string[] {
const allowedHosts = new Set<string>()
for (const allowedHost of getHostSources(host, extraHosts)) {
allowedHosts.add(allowedHost)
allowedHosts.add(`${allowedHost}:${port}`)
}
return [...allowedHosts]
}
function getAllowedOrigins(host: string, port: number, extraHosts: string[]): string[] {
const allowedOrigins = new Set<string>()
for (const allowedHost of getHostSources(host, extraHosts)) {
allowedOrigins.add(`http://${formatHostForUrl(allowedHost)}:${port}`)
}
return [...allowedOrigins]
}
function getHostSources(host: string, extraHosts: string[]): string[] {
const normalizedHosts = new Set<string>()
for (const allowedHost of ['localhost', '127.0.0.1', '[::1]', host, ...extraHosts]) {
normalizedHosts.add(normalizeHostHeader(allowedHost))
}
return [...normalizedHosts]
}
function isLoopbackHost(host: string): boolean {
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'
}
function displayHostForBinding(host: string): string {
if (host === '0.0.0.0') {
return '127.0.0.1'
}
if (host === '::') {
return '::1'
}
return host
}
function normalizeHostHeader(host: string): string {
if (host.includes(':') && !host.startsWith('[')) {
return `[${host}]`
}
return host
}
function formatHostForUrl(host: string): string {
if (host.startsWith('[') && host.endsWith(']')) {
return host
}
if (host.includes(':')) {
return `[${host}]`
}
return host
}