@@ -41,13 +41,40 @@ app.use(
41
41
42
42
app . use ( express . json ( ) ) ;
43
43
44
+ // Helper function to create JSON-RPC error responses
45
+ function createJsonRpcError ( code : number , message : string ) {
46
+ return {
47
+ jsonrpc : "2.0" as const ,
48
+ error : { code, message } ,
49
+ id : null ,
50
+ } ;
51
+ }
52
+
53
+ // Get allowed HackMD API URLs from environment
54
+ function getAllowedApiUrls ( ) : string [ ] {
55
+ const allowedUrls = process . env . ALLOWED_HACKMD_API_URLS ;
56
+ if ( ! allowedUrls || allowedUrls . trim ( ) . length === 0 ) {
57
+ return [ DEFAULT_HACKMD_API_URL ] ;
58
+ }
59
+ return allowedUrls
60
+ . split ( "," )
61
+ . map ( ( url ) => url . trim ( ) )
62
+ . filter ( ( url ) => url . length > 0 ) ;
63
+ }
64
+
65
+ // Validate if the provided API URL is allowed
66
+ function isAllowedApiUrl ( url : string ) : boolean {
67
+ const allowedUrls = getAllowedApiUrls ( ) ;
68
+ return allowedUrls . includes ( url ) ;
69
+ }
70
+
44
71
// Parse configuration from header or query parameters (for Smithery)
45
- function parseConfig ( req : Request ) {
72
+ function parseConfig ( req : Request ) : { config ? : any ; error ? : any } {
46
73
const hackmdApiTokenHeader =
47
74
req . headers [ HACKMD_API_TOKEN_HEADER . toLowerCase ( ) ] ;
48
75
const hackmdApiUrlHeader = req . headers [ HACKMD_API_URL_HEADER . toLowerCase ( ) ] ;
49
76
50
- let config : any = { } ;
77
+ const config : any = { } ;
51
78
52
79
if (
53
80
typeof hackmdApiTokenHeader === "string" &&
@@ -65,20 +92,30 @@ function parseConfig(req: Request) {
65
92
66
93
// If any config found in headers, return it
67
94
if ( Object . keys ( config ) . length > 0 ) {
68
- return config ;
95
+ return { config } ;
69
96
}
70
97
71
98
// Smithery passes config as base64-encoded JSON in query parameters
72
99
const configParam = req . query . config ;
73
100
if ( typeof configParam === "string" && configParam . trim ( ) . length > 0 ) {
74
- const smitheryConfig = JSON . parse (
75
- Buffer . from ( configParam , "base64" ) . toString ( ) ,
76
- ) ;
77
- return smitheryConfig ;
101
+ try {
102
+ const smitheryConfig = JSON . parse (
103
+ Buffer . from ( configParam , "base64" ) . toString ( ) ,
104
+ ) ;
105
+
106
+ return { config : smitheryConfig } ;
107
+ } catch ( error ) {
108
+ return {
109
+ error : createJsonRpcError (
110
+ - 32000 ,
111
+ "Bad Request: Invalid base64-encoded config parameter" ,
112
+ ) ,
113
+ } ;
114
+ }
78
115
}
79
116
80
117
// Return empty config if nothing found
81
- return config ;
118
+ return { config } ;
82
119
}
83
120
84
121
// Create MCP server with HackMD integration
@@ -100,31 +137,53 @@ function createServer({ config }: { config: z.infer<typeof ConfigSchema> }) {
100
137
// Handle MCP requests at /mcp endpoint
101
138
app . post ( "/mcp" , async ( req : Request , res : Response ) => {
102
139
try {
103
- // Parse configuration
104
- const rawConfig = parseConfig ( req ) ;
140
+ // Parse configuration with URL validation
141
+ const parseResult = parseConfig ( req ) ;
142
+
143
+ // Check for parsing errors
144
+ if ( parseResult . error ) {
145
+ return res . status ( 400 ) . json ( parseResult . error ) ;
146
+ }
147
+
148
+ const rawConfig = parseResult . config || { } ;
105
149
106
150
// Check if API token is available (from header, query param, or env var)
107
151
const hackmdApiToken =
108
152
rawConfig . hackmdApiToken || process . env . HACKMD_API_TOKEN ;
109
153
110
154
if ( ! hackmdApiToken || hackmdApiToken . trim ( ) . length === 0 ) {
111
- return res . status ( 400 ) . json ( {
112
- jsonrpc : "2.0" ,
113
- error : {
114
- code : - 32000 ,
115
- message : `Bad Request: Please provide a HackMD API token via header '${ HACKMD_API_TOKEN_HEADER } '.` ,
116
- } ,
117
- id : null ,
118
- } ) ;
155
+ return res
156
+ . status ( 400 )
157
+ . json (
158
+ createJsonRpcError (
159
+ - 32000 ,
160
+ `Bad Request: Please provide a HackMD API token via header '${ HACKMD_API_TOKEN_HEADER } '.` ,
161
+ ) ,
162
+ ) ;
163
+ }
164
+
165
+ // Extract API URL from config or use default
166
+ const hackmdApiUrl =
167
+ rawConfig . hackmdApiUrl ||
168
+ process . env . HACKMD_API_URL ||
169
+ DEFAULT_HACKMD_API_URL ;
170
+
171
+ // Validation of the API URL
172
+ if ( ! isAllowedApiUrl ( hackmdApiUrl ) ) {
173
+ return res
174
+ . status ( 400 )
175
+ . json (
176
+ createJsonRpcError (
177
+ - 32000 ,
178
+ `Bad Request: HackMD API URL "${ hackmdApiUrl } " is not in the allowed list` ,
179
+ ) ,
180
+ ) ;
119
181
}
120
182
121
- // Validate and parse configuration with fallbacks to environment variables
183
+ // Validate and parse configuration
122
184
const config = ConfigSchema . parse ( {
123
185
hackmdApiToken,
124
- hackmdApiUrl :
125
- rawConfig . hackmdApiUrl ||
126
- process . env . HACKMD_API_URL ||
127
- DEFAULT_HACKMD_API_URL ,
186
+ hackmdApiUrl,
128
187
} ) ;
129
188
130
189
const server = createServer ( { config } ) ;
@@ -143,41 +202,23 @@ app.post("/mcp", async (req: Request, res: Response) => {
143
202
} catch ( error ) {
144
203
console . error ( "Error handling MCP request:" , error ) ;
145
204
if ( ! res . headersSent ) {
146
- res . status ( 500 ) . json ( {
147
- jsonrpc : "2.0" ,
148
- error : { code : - 32603 , message : "Internal server error" } ,
149
- id : null ,
150
- } ) ;
205
+ res . status ( 500 ) . json ( createJsonRpcError ( - 32603 , "Internal server error" ) ) ;
151
206
}
152
207
}
153
208
} ) ;
154
209
155
210
// SSE notifications not supported in stateless mode
156
211
app . get ( "/mcp" , async ( req : Request , res : Response ) => {
157
- res . writeHead ( 405 ) . end (
158
- JSON . stringify ( {
159
- jsonrpc : "2.0" ,
160
- error : {
161
- code : - 32000 ,
162
- message : "Method not allowed." ,
163
- } ,
164
- id : null ,
165
- } ) ,
166
- ) ;
212
+ res
213
+ . writeHead ( 405 )
214
+ . end ( JSON . stringify ( createJsonRpcError ( - 32000 , "Method not allowed." ) ) ) ;
167
215
} ) ;
168
216
169
217
// Session termination not needed in stateless mode
170
218
app . delete ( "/mcp" , async ( req : Request , res : Response ) => {
171
- res . writeHead ( 405 ) . end (
172
- JSON . stringify ( {
173
- jsonrpc : "2.0" ,
174
- error : {
175
- code : - 32000 ,
176
- message : "Method not allowed." ,
177
- } ,
178
- id : null ,
179
- } ) ,
180
- ) ;
219
+ res
220
+ . writeHead ( 405 )
221
+ . end ( JSON . stringify ( createJsonRpcError ( - 32000 , "Method not allowed." ) ) ) ;
181
222
} ) ;
182
223
183
224
// Main function to start the server in the appropriate mode
0 commit comments