@meldocio/mcp-stdio-proxy 1.0.28 → 1.1.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.
@@ -0,0 +1,279 @@
1
+ /**
2
+ * OAuth 2.1 PKCE Authentication Flow
3
+ *
4
+ * Implements OAuth 2.1 authorization code flow with PKCE for agent authentication.
5
+ * Uses a loopback redirect URI per RFC 8252.
6
+ *
7
+ * Server endpoints:
8
+ * POST /mcp/oauth/register - dynamic client registration
9
+ * GET /mcp/oauth/authorize - authorization page (in browser)
10
+ * POST /mcp/oauth/token - code exchange + refresh token rotation
11
+ */
12
+
13
+ const crypto = require('crypto')
14
+ const http = require('http')
15
+ const axios = require('axios')
16
+ const https = require('https')
17
+ const { URL, URLSearchParams } = require('url')
18
+ const { writeCredentials } = require('./credentials')
19
+ const { getApiUrl, getAppUrl } = require('./constants')
20
+ const { exec } = require('child_process')
21
+
22
+ /**
23
+ * Generate a cryptographically random PKCE code verifier (base64url, 43-128 chars).
24
+ * @returns {string}
25
+ */
26
+ function generateCodeVerifier() {
27
+ return crypto.randomBytes(32).toString('base64url')
28
+ }
29
+
30
+ /**
31
+ * Derive code challenge from verifier using S256 method.
32
+ * @param {string} verifier
33
+ * @returns {string} Base64url-encoded SHA-256 hash
34
+ */
35
+ function generateCodeChallenge(verifier) {
36
+ return crypto.createHash('sha256').update(verifier).digest('base64url')
37
+ }
38
+
39
+ /**
40
+ * Start a local HTTP server on a random loopback port to receive the OAuth redirect.
41
+ * @returns {Promise<{server, port, getCode}>}
42
+ */
43
+ function startCallbackServer() {
44
+ return new Promise((resolve, reject) => {
45
+ let resolveCode
46
+ const codePromise = new Promise((res) => { resolveCode = res })
47
+
48
+ const server = http.createServer((req, res) => {
49
+ const url = new URL(req.url, 'http://127.0.0.1')
50
+ const code = url.searchParams.get('code')
51
+ const state = url.searchParams.get('state')
52
+ const error = url.searchParams.get('error')
53
+
54
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
55
+ if (error) {
56
+ res.end('<html><body><h2>Authentication failed: ' + error + '</h2><p>You can close this tab.</p></body></html>')
57
+ } else {
58
+ res.end('<html><body><h2>Authentication successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>')
59
+ }
60
+
61
+ resolveCode({ code, state, error })
62
+ })
63
+
64
+ server.listen(0, '127.0.0.1', () => {
65
+ resolve({
66
+ server,
67
+ port: server.address().port,
68
+ getCode: () => codePromise
69
+ })
70
+ })
71
+
72
+ server.on('error', reject)
73
+ })
74
+ }
75
+
76
+ /**
77
+ * Open a URL in the default browser (best-effort, ignores errors).
78
+ * @param {string} url
79
+ */
80
+ function openBrowser(url) {
81
+ let command
82
+ if (process.platform === 'darwin') {
83
+ command = `open "${url}"`
84
+ } else if (process.platform === 'win32') {
85
+ command = `start "" "${url}"`
86
+ } else {
87
+ command = `xdg-open "${url}"`
88
+ }
89
+ exec(command, () => {}) // Ignore errors
90
+ }
91
+
92
+ /**
93
+ * Register a new OAuth client with the server (dynamic client registration).
94
+ * @param {string} apiBaseUrl
95
+ * @param {string} redirectUri
96
+ * @returns {Promise<string>} client_id
97
+ */
98
+ async function registerClient(apiBaseUrl, redirectUri) {
99
+ const response = await axios.post(`${apiBaseUrl}/mcp/oauth/register`, {
100
+ redirect_uris: [redirectUri],
101
+ client_name: 'meldoc-mcp-proxy'
102
+ }, {
103
+ timeout: 10000,
104
+ httpsAgent: new https.Agent({ keepAlive: true })
105
+ })
106
+
107
+ if (!response.data.client_id) {
108
+ throw new Error('OAuth registration failed: no client_id in response')
109
+ }
110
+
111
+ return response.data.client_id
112
+ }
113
+
114
+ /**
115
+ * Exchange an authorization code for access + refresh tokens.
116
+ * @param {string} apiBaseUrl
117
+ * @param {string} code
118
+ * @param {string} codeVerifier
119
+ * @param {string} clientId
120
+ * @param {string} redirectUri
121
+ * @returns {Promise<{accessToken, refreshToken, expiresAt}>}
122
+ */
123
+ async function exchangeCode(apiBaseUrl, code, codeVerifier, clientId, redirectUri) {
124
+ const params = new URLSearchParams({
125
+ grant_type: 'authorization_code',
126
+ code,
127
+ code_verifier: codeVerifier,
128
+ client_id: clientId,
129
+ redirect_uri: redirectUri
130
+ })
131
+
132
+ const response = await axios.post(`${apiBaseUrl}/mcp/oauth/token`, params.toString(), {
133
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
134
+ timeout: 10000,
135
+ httpsAgent: new https.Agent({ keepAlive: true })
136
+ })
137
+
138
+ const data = response.data
139
+ if (!data.access_token) {
140
+ throw new Error('Token exchange failed: no access_token in response')
141
+ }
142
+
143
+ return {
144
+ accessToken: data.access_token,
145
+ refreshToken: data.refresh_token || null,
146
+ expiresAt: new Date(Date.now() + (data.expires_in || 3600) * 1000).toISOString()
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Refresh tokens using OAuth 2.1 refresh token rotation.
152
+ * The server issues a new refresh token on each use (old token is revoked).
153
+ * @param {string} apiBaseUrl
154
+ * @param {string} refreshToken
155
+ * @param {string} clientId
156
+ * @returns {Promise<{accessToken, refreshToken, expiresAt}>}
157
+ */
158
+ async function refreshOAuthToken(apiBaseUrl, refreshToken, clientId) {
159
+ const params = new URLSearchParams({
160
+ grant_type: 'refresh_token',
161
+ refresh_token: refreshToken,
162
+ client_id: clientId
163
+ })
164
+
165
+ const response = await axios.post(`${apiBaseUrl}/mcp/oauth/token`, params.toString(), {
166
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
167
+ timeout: 10000,
168
+ httpsAgent: new https.Agent({ keepAlive: true })
169
+ })
170
+
171
+ const data = response.data
172
+ if (!data.access_token) {
173
+ throw new Error('Token refresh failed: no access_token in response')
174
+ }
175
+
176
+ return {
177
+ accessToken: data.access_token,
178
+ refreshToken: data.refresh_token || refreshToken, // Server rotates the token
179
+ expiresAt: new Date(Date.now() + (data.expires_in || 3600) * 1000).toISOString()
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Perform interactive OAuth 2.1 PKCE login.
185
+ * Opens browser, starts loopback callback server, exchanges code for tokens.
186
+ * @param {Object} options
187
+ * @param {string} [options.apiBaseUrl]
188
+ * @param {string} [options.appUrl]
189
+ * @param {number} [options.timeout=120000]
190
+ * @returns {Promise<Object>} Credentials object
191
+ */
192
+ async function pkceLogin(options = {}) {
193
+ const {
194
+ apiBaseUrl = null,
195
+ appUrl = null,
196
+ timeout = 120000
197
+ } = options
198
+
199
+ const apiUrl = apiBaseUrl || getApiUrl()
200
+ const frontendUrl = appUrl || getAppUrl()
201
+
202
+ const { server, port, getCode } = await startCallbackServer()
203
+ const redirectUri = `http://127.0.0.1:${port}/callback`
204
+
205
+ try {
206
+ process.stderr.write('Registering OAuth client...\n')
207
+ const clientId = await registerClient(apiUrl, redirectUri)
208
+
209
+ const codeVerifier = generateCodeVerifier()
210
+ const codeChallenge = generateCodeChallenge(codeVerifier)
211
+ const state = crypto.randomBytes(16).toString('hex')
212
+
213
+ const authParams = new URLSearchParams({
214
+ client_id: clientId,
215
+ redirect_uri: redirectUri,
216
+ code_challenge: codeChallenge,
217
+ code_challenge_method: 'S256',
218
+ state,
219
+ response_type: 'code'
220
+ })
221
+
222
+ const authUrl = `${frontendUrl}/mcp/oauth/authorize?${authParams.toString()}`
223
+
224
+ process.stderr.write('\n╔═══════════════════════════════════════════════════════╗\n')
225
+ process.stderr.write('║ ║\n')
226
+ process.stderr.write('║ 🔐 Meldoc Authentication Required ║\n')
227
+ process.stderr.write('║ ║\n')
228
+ process.stderr.write('╚═══════════════════════════════════════════════════════╝\n\n')
229
+ process.stderr.write(`🌐 Visit: ${authUrl}\n\n`)
230
+ process.stderr.write('🚀 Opening browser automatically...\n\n')
231
+
232
+ openBrowser(authUrl)
233
+
234
+ process.stderr.write('⏳ Waiting for authorization...\n\n')
235
+
236
+ const timeoutPromise = new Promise((_, reject) =>
237
+ setTimeout(() => reject(new Error('Authentication timeout. Please try again.')), timeout)
238
+ )
239
+
240
+ const { code, state: returnedState, error: callbackError } = await Promise.race([
241
+ getCode(),
242
+ timeoutPromise
243
+ ])
244
+
245
+ if (callbackError) throw new Error('OAuth error: ' + callbackError)
246
+ if (returnedState !== state) throw new Error('OAuth state mismatch - possible CSRF attack')
247
+ if (!code) throw new Error('No authorization code received')
248
+
249
+ const tokens = await exchangeCode(apiUrl, code, codeVerifier, clientId, redirectUri)
250
+
251
+ const credentials = {
252
+ type: 'user_session',
253
+ auth_method: 'pkce',
254
+ client_id: clientId,
255
+ apiBaseUrl: apiUrl,
256
+ tokens: {
257
+ accessToken: tokens.accessToken,
258
+ accessExpiresAt: tokens.expiresAt,
259
+ refreshToken: tokens.refreshToken
260
+ },
261
+ updatedAt: new Date().toISOString()
262
+ }
263
+
264
+ writeCredentials(credentials)
265
+
266
+ process.stderr.write('\n✅ Successfully authenticated!\n\n')
267
+
268
+ return credentials
269
+ } finally {
270
+ server.close()
271
+ }
272
+ }
273
+
274
+ module.exports = {
275
+ pkceLogin,
276
+ refreshOAuthToken,
277
+ generateCodeVerifier,
278
+ generateCodeChallenge
279
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Streamable HTTP Proxy for MCP
3
+ *
4
+ * Implements MCP Streamable HTTP transport (spec 2025-03-26).
5
+ * Mirrors the Go implementation in meldoc.cli/cli/internal/mcp/proxy.go
6
+ */
7
+
8
+ const https = require('https')
9
+ const http = require('http')
10
+ const { URL } = require('url')
11
+
12
+ const REQUEST_TIMEOUT = 60000
13
+
14
+ /**
15
+ * StreamableHTTPProxy forwards JSON-RPC requests from stdio to a remote MCP HTTP server.
16
+ */
17
+ class StreamableHTTPProxy {
18
+ /**
19
+ * @param {string} remoteURL - Full URL of the MCP endpoint (e.g. https://api.meldoc.io/mcp)
20
+ * @param {Function} getToken - Async function that returns the auth token string (or null)
21
+ */
22
+ constructor(remoteURL, getToken) {
23
+ this.remoteURL = remoteURL
24
+ this.getToken = getToken
25
+ this.sessionId = null
26
+ }
27
+
28
+ /**
29
+ * Send the initialize request and store the session ID.
30
+ * @param {*} id - Request ID
31
+ * @param {*} params - Initialize params (may be null)
32
+ * @returns {Promise<Object>} Full JSON-RPC response object (not just result)
33
+ */
34
+ async initialize(id, params) {
35
+ const body = { jsonrpc: '2.0', id, method: 'initialize' }
36
+ if (params) body.params = params
37
+
38
+ const { response, newSessionId } = await this._doRequest(body, '')
39
+
40
+ if (newSessionId) this.sessionId = newSessionId
41
+
42
+ const text = await this._readBody(response)
43
+ let parsed
44
+ try {
45
+ parsed = JSON.parse(text)
46
+ } catch (e) {
47
+ throw new Error('Failed to parse initialize response: ' + text)
48
+ }
49
+
50
+ if (parsed.error) {
51
+ throw new Error(`Initialize error ${parsed.error.code}: ${parsed.error.message}`)
52
+ }
53
+
54
+ return parsed
55
+ }
56
+
57
+ /**
58
+ * Forward a JSON-RPC request to the remote server and write all responses to stdout.
59
+ * Handles session expiry (HTTP 404) by re-initializing and retrying once.
60
+ * For SSE responses each event is written as a separate newline-delimited line.
61
+ * @param {Object} request - JSON-RPC request object
62
+ */
63
+ async forwardAndWrite(request) {
64
+ const body = {
65
+ jsonrpc: request.jsonrpc || '2.0',
66
+ id: request.id,
67
+ method: request.method
68
+ }
69
+ if (request.params !== undefined) body.params = request.params
70
+
71
+ let result
72
+ try {
73
+ result = await this._doRequest(body, this.sessionId || '')
74
+ } catch (err) {
75
+ if (err.statusCode === 404) {
76
+ // Session expired — re-initialize and retry once
77
+ try {
78
+ await this.initialize(0, null)
79
+ } catch (reinitErr) {
80
+ this._writeError(request.id, -32603, 'Session re-initialization failed: ' + reinitErr.message)
81
+ return
82
+ }
83
+ try {
84
+ result = await this._doRequest(body, this.sessionId || '')
85
+ } catch (retryErr) {
86
+ this._writeError(request.id, -32603, retryErr.message)
87
+ return
88
+ }
89
+ } else if (err.statusCode === 401 || err.statusCode === 403) {
90
+ this._writeError(request.id, -32002, 'Authentication failed: ' + (err.body || 'unauthorized'))
91
+ return
92
+ } else {
93
+ this._writeError(request.id, -32603, err.message)
94
+ return
95
+ }
96
+ }
97
+
98
+ const { response, newSessionId } = result
99
+ if (newSessionId) this.sessionId = newSessionId
100
+
101
+ const contentType = response.headers['content-type'] || ''
102
+ if (contentType.includes('text/event-stream')) {
103
+ await this._streamSSE(response)
104
+ } else {
105
+ const text = await this._readBody(response)
106
+ if (text.trim()) process.stdout.write(text.trim() + '\n')
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Perform the raw HTTP POST to the remote server.
112
+ * @param {Object} body - JSON body to send
113
+ * @param {string} sessionId - Session ID header value (empty string to omit)
114
+ * @returns {Promise<{response: IncomingMessage, newSessionId: string}>}
115
+ */
116
+ async _doRequest(body, sessionId) {
117
+ const token = await this.getToken()
118
+ const data = JSON.stringify(body)
119
+ const url = new URL(this.remoteURL)
120
+ const isHttps = url.protocol === 'https:'
121
+ const port = url.port ? parseInt(url.port) : (isHttps ? 443 : 80)
122
+
123
+ const headers = {
124
+ 'Content-Type': 'application/json',
125
+ 'Accept': 'application/json, text/event-stream',
126
+ 'Content-Length': Buffer.byteLength(data)
127
+ }
128
+
129
+ if (token) headers['Authorization'] = 'Bearer ' + token
130
+ if (sessionId) headers['Mcp-Session-Id'] = sessionId
131
+
132
+ return new Promise((resolve, reject) => {
133
+ const options = {
134
+ hostname: url.hostname,
135
+ port,
136
+ path: url.pathname + (url.search || ''),
137
+ method: 'POST',
138
+ headers,
139
+ timeout: REQUEST_TIMEOUT
140
+ }
141
+
142
+ const mod = isHttps ? https : http
143
+ const req = mod.request(options, (res) => {
144
+ const newSessionId = res.headers['mcp-session-id'] || ''
145
+
146
+ if (res.statusCode < 200 || res.statusCode >= 300) {
147
+ let errBody = ''
148
+ res.on('data', (chunk) => { errBody += chunk })
149
+ res.on('end', () => {
150
+ const err = new Error(`HTTP ${res.statusCode}: ${errBody.trim()}`)
151
+ err.statusCode = res.statusCode
152
+ err.body = errBody.trim()
153
+ reject(err)
154
+ })
155
+ return
156
+ }
157
+
158
+ resolve({ response: res, newSessionId })
159
+ })
160
+
161
+ req.on('timeout', () => {
162
+ req.destroy()
163
+ const err = new Error('Request timeout')
164
+ err.statusCode = 408
165
+ reject(err)
166
+ })
167
+
168
+ req.on('error', reject)
169
+
170
+ req.write(data)
171
+ req.end()
172
+ })
173
+ }
174
+
175
+ /**
176
+ * Stream an SSE response, writing each data event as a line to stdout.
177
+ * @param {IncomingMessage} response
178
+ */
179
+ _streamSSE(response) {
180
+ return new Promise((resolve, reject) => {
181
+ let buffer = ''
182
+
183
+ response.on('data', (chunk) => {
184
+ buffer += chunk.toString()
185
+ const lines = buffer.split('\n')
186
+ buffer = lines.pop() // Keep incomplete last line
187
+
188
+ for (const line of lines) {
189
+ if (line.startsWith('data: ')) {
190
+ const data = line.slice(6)
191
+ if (data && data !== '[DONE]') process.stdout.write(data + '\n')
192
+ }
193
+ }
194
+ })
195
+
196
+ response.on('end', () => {
197
+ if (buffer.startsWith('data: ')) {
198
+ const data = buffer.slice(6)
199
+ if (data && data !== '[DONE]') process.stdout.write(data + '\n')
200
+ }
201
+ resolve()
202
+ })
203
+
204
+ response.on('error', reject)
205
+ })
206
+ }
207
+
208
+ /**
209
+ * Read full response body as a string.
210
+ * @param {IncomingMessage} response
211
+ * @returns {Promise<string>}
212
+ */
213
+ _readBody(response) {
214
+ return new Promise((resolve, reject) => {
215
+ let body = ''
216
+ response.on('data', (chunk) => { body += chunk })
217
+ response.on('end', () => resolve(body))
218
+ response.on('error', reject)
219
+ })
220
+ }
221
+
222
+ /**
223
+ * Write a JSON-RPC error to stdout.
224
+ */
225
+ _writeError(id, code, message) {
226
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n')
227
+ }
228
+ }
229
+
230
+ module.exports = { StreamableHTTPProxy }
@@ -1,81 +1,15 @@
1
1
  /**
2
2
  * MCP Protocol Method Handlers
3
3
  *
4
- * Handles MCP protocol methods (initialize, ping, tools/list, etc.)
5
- * These are handled locally and not proxied to the backend.
4
+ * Handles only the minimal set of MCP protocol methods that must be
5
+ * processed locally (notifications and ping).
6
+ * initialize, tools/list and all other methods are forwarded to the server.
6
7
  */
7
8
 
8
- const { sendResponse, sendError } = require('../protocol/json-rpc');
9
- const { getToolsList } = require('../protocol/tools-schema');
10
- const { JSON_RPC_ERROR_CODES } = require('../protocol/error-codes');
11
- const { LOG_LEVELS, MCP_PROTOCOL_VERSION, SERVER_CAPABILITIES } = require('../core/constants');
9
+ const { sendResponse } = require('../protocol/json-rpc');
12
10
 
13
11
  /**
14
- * Get log level
15
- */
16
- function getLogLevel() {
17
- const level = (process.env.LOG_LEVEL || 'ERROR').toUpperCase();
18
- return LOG_LEVELS[level] !== undefined ? LOG_LEVELS[level] : LOG_LEVELS.ERROR;
19
- }
20
-
21
- const LOG_LEVEL = getLogLevel();
22
-
23
- /**
24
- * Log message
25
- */
26
- function log(level, message) {
27
- if (LOG_LEVEL >= level) {
28
- const levelName = Object.keys(LOG_LEVELS)[level] || 'UNKNOWN';
29
- process.stderr.write(`[${levelName}] ${message}\n`);
30
- }
31
- }
32
-
33
- /**
34
- * Get package info
35
- */
36
- function getPackageInfo() {
37
- try {
38
- const pkg = require('../../package.json');
39
- return { name: pkg.name, version: pkg.version };
40
- } catch (error) {
41
- return { name: '@meldocio/mcp-stdio-proxy', version: '1.0.0' };
42
- }
43
- }
44
-
45
- /**
46
- * Handle MCP initialize method
47
- * @param {Object} request - JSON-RPC request
48
- */
49
- function handleInitialize(request) {
50
- const pkg = getPackageInfo();
51
-
52
- const result = {
53
- protocolVersion: MCP_PROTOCOL_VERSION,
54
- capabilities: {
55
- tools: SERVER_CAPABILITIES.TOOLS ? {} : undefined,
56
- resources: SERVER_CAPABILITIES.RESOURCES ? {} : undefined,
57
- prompts: SERVER_CAPABILITIES.PROMPTS ? {} : undefined,
58
- logging: SERVER_CAPABILITIES.LOGGING ? {} : undefined
59
- },
60
- serverInfo: {
61
- name: pkg.name,
62
- version: pkg.version
63
- }
64
- };
65
-
66
- // Remove undefined capabilities
67
- Object.keys(result.capabilities).forEach(key => {
68
- if (result.capabilities[key] === undefined) {
69
- delete result.capabilities[key];
70
- }
71
- });
72
-
73
- log(LOG_LEVELS.DEBUG, 'Initialize request received');
74
- sendResponse(request.id, result);
75
- }
76
-
77
- /**
78
- * Handle MCP ping method (keep-alive)
12
+ * Handle MCP ping method (keep-alive).
79
13
  * @param {Object} request - JSON-RPC request
80
14
  */
81
15
  function handlePing(request) {
@@ -83,103 +17,20 @@ function handlePing(request) {
83
17
  }
84
18
 
85
19
  /**
86
- * Handle resources/list method
87
- * Returns empty list as resources are not supported yet
88
- * @param {Object} request - JSON-RPC request
20
+ * Check if a method is a notification that requires no response.
21
+ * @param {string} method
22
+ * @returns {boolean}
89
23
  */
90
- function handleResourcesList(request) {
91
- sendResponse(request.id, {
92
- resources: []
93
- });
94
- }
95
-
96
- /**
97
- * Handle tools/list method
98
- * Always returns static list locally, never proxies to backend
99
- * @param {Object} request - JSON-RPC request
100
- */
101
- function handleToolsList(request) {
102
- try {
103
- const tools = getToolsList();
104
-
105
- // Log tool names for debugging
106
- const toolNames = tools.map(t => t.name).join(', ');
107
- log(LOG_LEVELS.INFO, `Returning ${tools.length} tools locally: ${toolNames}`);
108
-
109
- sendResponse(request.id, {
110
- tools: tools
111
- });
112
- } catch (error) {
113
- // This should never happen, but if it does, send error response
114
- log(LOG_LEVELS.ERROR, `Unexpected error in handleToolsList: ${error.message || 'Unknown error'}`);
115
- log(LOG_LEVELS.DEBUG, `Error stack: ${error.stack || 'No stack trace'}`);
116
-
117
- sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
118
- `Failed to get tools list: ${error.message || 'Unknown error'}`, {
119
- code: 'INTERNAL_ERROR'
120
- });
121
- }
122
- }
123
-
124
- /**
125
- * Check if a method should be handled locally
126
- * @param {string} method - Method name
127
- * @returns {boolean} True if should be handled locally
128
- */
129
- function isLocalMethod(method) {
130
- const localMethods = [
131
- 'initialize',
132
- 'initialized',
133
- 'notifications/initialized',
134
- 'notifications/cancelled',
135
- 'ping',
136
- 'resources/list',
137
- 'tools/list'
138
- ];
139
- return localMethods.includes(method);
140
- }
141
-
142
- /**
143
- * Route request to appropriate local handler
144
- * @param {Object} request - JSON-RPC request
145
- * @returns {boolean} True if handled, false if should be proxied
146
- */
147
- function handleLocalMethod(request) {
148
- const method = request.method;
149
-
150
- switch (method) {
151
- case 'initialize':
152
- handleInitialize(request);
153
- return true;
154
-
155
- case 'initialized':
156
- case 'notifications/initialized':
157
- case 'notifications/cancelled':
158
- // Notifications - no response needed
159
- return true;
160
-
161
- case 'ping':
162
- handlePing(request);
163
- return true;
164
-
165
- case 'resources/list':
166
- handleResourcesList(request);
167
- return true;
168
-
169
- case 'tools/list':
170
- handleToolsList(request);
171
- return true;
172
-
173
- default:
174
- return false; // Not a local method
175
- }
24
+ function isNotification(method) {
25
+ return (
26
+ method === 'initialized' ||
27
+ method === 'notifications/initialized' ||
28
+ method === 'notifications/cancelled' ||
29
+ method === 'notifications/progress'
30
+ );
176
31
  }
177
32
 
178
33
  module.exports = {
179
- handleInitialize,
180
34
  handlePing,
181
- handleResourcesList,
182
- handleToolsList,
183
- isLocalMethod,
184
- handleLocalMethod
35
+ isNotification
185
36
  };