@dahawa/hawa-cli-analysis 1.0.4

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,556 @@
1
+ /**
2
+ * OAuth-transparent MCP Client (Node.js, JavaScript)
3
+ *
4
+ * Goals:
5
+ * - Same high-level interface as `@modelcontextprotocol/sdk/client`'s Client:
6
+ * - connect(transport)
7
+ * - listPrompts()
8
+ * - getPrompt({ name, arguments })
9
+ * - listResources()
10
+ * - readResource({ uri })
11
+ * - listTools()
12
+ * - callTool({ name, arguments })
13
+ * - No dependency on `@modelcontextprotocol/sdk/client`.
14
+ * - Adds automatic OAuth 2.0 (Authorization Code + PKCE) sign-in.
15
+ * The client detects missing/expired auth and performs the flow transparently.
16
+ * - Supports OAuth metadata discovery (RFC 8414) and Dynamic Client Registration (RFC 7591).
17
+ *
18
+ * Transport supported here:
19
+ * - Streamable HTTP (JSON-RPC 2.0 over HTTP). Stdio can be added by implementing
20
+ * a Transport with a `send(method, params)` function and passing into Client.connect().
21
+ *
22
+ * Usage Example (Cloudflare Radar, auto discovery + auto registration):
23
+ * import { Client, StreamableHTTPClientTransport } from './mcp-client.js'
24
+ *
25
+ * const issuer = 'https://radar.mcp.cloudflare.com'
26
+ * const transport = new StreamableHTTPClientTransport(`${issuer}/mcp`, {
27
+ * oauth: {
28
+ * issuer, // auto-discovery of /.well-known/oauth-authorization-server
29
+ * // discoveryUrl: `${issuer}/.well-known/oauth-authorization-server`, // optional explicit
30
+ * // Do not provide clientId => dynamic client registration to /register
31
+ * scopes: ['openid','profile','email','offline_access'],
32
+ * redirectUri: 'http://127.0.0.1:53175/callback',
33
+ * tokenStorePath: '.mcp_oauth_tokens.json',
34
+ * clientName: 'demo-auto-reg',
35
+ * debug: true, // enable helpful logs
36
+ * authTimeoutMs: 180000 // 3 minutes timeout to avoid hanging
37
+ * }
38
+ * })
39
+ *
40
+ * const client = new Client({ name: 'radar-demo', version: '1.0.0' })
41
+ * await client.connect(transport)
42
+ * console.log(await client.listTools())
43
+ * // const result = await client.callTool({ name: 'some_tool', arguments: {} })
44
+ * // console.log(result)
45
+ */
46
+
47
+ import { createServer } from 'node:http'
48
+ import { randomBytes, createHash } from 'node:crypto'
49
+ import { spawn } from 'node:child_process'
50
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs'
51
+ import { URL, URLSearchParams } from 'node:url'
52
+ import LogManager from "../logger-manager.js";
53
+ const logger = LogManager.getSystemLogger();
54
+
55
+ /** Utility: open system browser cross-platform */
56
+ function openInBrowser(url) {
57
+ const href = typeof url === 'string' ? url : url.href
58
+ const platform = process.platform
59
+ logger.debug('[oauth] opening browser at:' + platform , href);
60
+ try {
61
+ if (platform === 'darwin') {
62
+ spawn('open', [href], { stdio: 'ignore', detached: true }).unref()
63
+ } else if (platform === 'win32') {
64
+ spawn('cmd', ['/c', 'start', '""', href.replace(/&/g, '^&')], { stdio: 'ignore', detached: true }).unref()
65
+ } else {
66
+ const candidates = ['xdg-open', 'x-www-browser', 'gnome-open', 'kde-open']
67
+ spawn(candidates[0], [href], { stdio: 'ignore', detached: true }).unref()
68
+ }
69
+ } catch (e) {
70
+ logger.warn('[oauth] failed to open browser automatically. Please open this URL manually:', href)
71
+ }
72
+ }
73
+
74
+ /** Simple JSON file token store */
75
+ class TokenStore {
76
+ constructor(filePath = '.mcp_oauth_tokens.json') {
77
+ this.filePath = filePath
78
+ this.cache = existsSync(filePath) ? JSON.parse(readFileSync(filePath, 'utf-8')) : {}
79
+ }
80
+ get(key) { return this.cache[key] }
81
+ set(key, value) { this.cache[key] = value; writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2)) }
82
+ delete(key) { delete this.cache[key]; writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2)) }
83
+ }
84
+
85
+ /** OAuth2 PKCE helper with Discovery + Dynamic Client Registration */
86
+ class OAuthManager {
87
+ constructor({ issuer, discoveryUrl, authorizationUrl, tokenUrl, registrationUrl, clientId, scopes = [], redirectUri, tokenStorePath, clientName = 'mcp-client-js', debug = true, authTimeoutMs = 180000 }) {
88
+ // Config (some may be discovered later)
89
+ this.issuer = issuer ? new URL(issuer).origin : undefined
90
+ this.discoveryUrl = discoveryUrl ? new URL(discoveryUrl) : undefined
91
+ this.authorizationUrl = authorizationUrl ? new URL(authorizationUrl) : undefined
92
+ this.tokenUrl = tokenUrl ? new URL(tokenUrl) : undefined
93
+ this.registrationUrl = registrationUrl ? new URL(registrationUrl) : undefined
94
+ this.clientId = clientId
95
+ this.clientName = clientName
96
+ this.scopes = scopes
97
+ this.redirectUri = new URL(redirectUri)
98
+ this.store = new TokenStore(tokenStorePath)
99
+ this.debug = !!debug
100
+ this.authTimeoutMs = authTimeoutMs || 180000
101
+
102
+ // distinct keys so rotating server metadata doesn't clobber tokens
103
+ this.metaKey = `meta|${this.issuer || this.authorizationUrl?.origin || this.tokenUrl?.origin}`
104
+ this.storeKey = `tokens|${this.tokenUrl?.origin || this.issuer}|${this.clientId || 'auto'}`
105
+ }
106
+
107
+ /** Ensure we have server metadata (RFC 8414) and endpoints. */
108
+ async discoverIfNeeded() {
109
+ let meta = this.store.get(this.metaKey)
110
+ if (!this.authorizationUrl || !this.tokenUrl || !this.registrationUrl) {
111
+ // Try in-memory meta first
112
+ if (!meta) {
113
+ let base
114
+ if (this.discoveryUrl) {
115
+ base = this.discoveryUrl
116
+ } else if (this.issuer) {
117
+ base = new URL(`${this.issuer}/.well-known/oauth-authorization-server`)
118
+ } else if (this.authorizationUrl) {
119
+ base = new URL(`${this.authorizationUrl.origin}/.well-known/oauth-authorization-server`)
120
+ } else if (this.tokenUrl) {
121
+ base = new URL(`${this.tokenUrl.origin}/.well-known/oauth-authorization-server`)
122
+ } else {
123
+ throw new Error('OAuth discovery requires issuer, discoveryUrl, authorizationUrl, or tokenUrl')
124
+ }
125
+ const resp = await fetch(base)
126
+ if (!resp.ok) throw new Error(`OAuth discovery failed: ${resp.status}`)
127
+ meta = await resp.json()
128
+ this.store.set(this.metaKey, meta)
129
+ if (this.debug) logger.debug('[oauth] discovery metadata:', meta)
130
+ }
131
+ if (!this.authorizationUrl && meta.authorization_endpoint) this.authorizationUrl = new URL(meta.authorization_endpoint)
132
+ if (!this.tokenUrl && meta.token_endpoint) this.tokenUrl = new URL(meta.token_endpoint)
133
+ if (!this.registrationUrl && meta.registration_endpoint) this.registrationUrl = new URL(meta.registration_endpoint)
134
+
135
+ if (this.debug) {
136
+ logger.debug('[oauth] discovered endpoints:', {
137
+ authorization_endpoint: this.authorizationUrl?.href,
138
+ token_endpoint: this.tokenUrl?.href,
139
+ registration_endpoint: this.registrationUrl?.href || '(none)'
140
+ })
141
+ }
142
+ }
143
+ }
144
+ /**
145
+ * 从缓存中加载 client_id
146
+ */
147
+ async loadClientId() {
148
+ const regKey = `reg|${this.authorizationUrl.origin}|${this.redirectUri.href}`
149
+ const record = this.store.get(regKey)
150
+ if (record) {
151
+ logger.debug('[oauth] loaded client_id from cache:', record.client_id)
152
+ return record.client_id
153
+ }
154
+ return null
155
+ }
156
+
157
+
158
+ /** Ensure we have a client_id, using Dynamic Client Registration (RFC 7591) if needed. */
159
+ async ensureClientId() {
160
+ await this.discoverIfNeeded();
161
+ if (this.clientId) {
162
+ this.storeKey = `tokens|${this.tokenUrl.origin}|${this.clientId}`
163
+ return this.clientId
164
+ }
165
+
166
+ this.clientId = await this.loadClientId();
167
+ if(this.clientId){
168
+ this.storeKey = `tokens|${this.tokenUrl.origin}|${this.clientId}`
169
+ return this.clientId
170
+ }
171
+
172
+ if (!this.registrationUrl) {
173
+ if (this.debug) logger.warn('[oauth] no registration_endpoint; dynamic registration disabled. You must set oauth.clientId explicitly.')
174
+ throw new Error('Server does not advertise dynamic client registration; please supply oauth.clientId')
175
+ }
176
+
177
+ const body = {
178
+ client_name: this.clientName,
179
+ application_type: 'native', // desktop/cli style public client
180
+ redirect_uris: [this.redirectUri.href],
181
+ grant_types: ['authorization_code', 'refresh_token'],
182
+ response_types: ['code'],
183
+ token_endpoint_auth_method: 'none', // PKCE public client
184
+ scope: this.scopes.join(' '),
185
+ }
186
+ const resp = await fetch(this.registrationUrl, {
187
+ method: 'POST',
188
+ headers: { 'Content-Type': 'application/json' },
189
+ body: JSON.stringify(body)
190
+ })
191
+ if (!resp.ok) {
192
+ logger.error('[oauth] client registration failed:', await resp.text())
193
+ throw new Error(`Client registration failed: ${resp.status}`)
194
+ }
195
+ const json = await resp.json()
196
+ this.clientId = json.client_id
197
+ const regKey = `reg|${this.authorizationUrl.origin}|${this.redirectUri.href}`
198
+ this.store.set(regKey, { client_id: this.clientId, obtained_at: Date.now() })
199
+ this.storeKey = `tokens|${this.tokenUrl.origin}|${this.clientId}`
200
+ if (this.debug) logger.debug('[oauth] dynamic client registered:', this.clientId)
201
+ return this.clientId
202
+ }
203
+
204
+ /** Return a valid access token (refresh if needed, or run full flow). */
205
+ async getAccessToken() {
206
+ await this.discoverIfNeeded()
207
+ await this.ensureClientId().catch((e) => { throw e })
208
+ const record = this.store.get(this.storeKey)
209
+ if (record) {
210
+ const { access_token, expires_at, refresh_token } = record
211
+ // 检查访问令牌是否有效
212
+ if (expires_at && Date.now() < expires_at - 30_000) {
213
+ if (this.debug) logger.debug('[oauth] reuse cached token (valid)')
214
+ return access_token
215
+ }
216
+ // 如果访问令牌已过期,尝试使用刷新令牌获取新令牌
217
+ if (refresh_token) {
218
+ try {
219
+ if (this.debug) logger.debug('[oauth] refreshing access token...')
220
+ return await this.refresh(refresh_token)
221
+ } catch (err) {
222
+ if (this.debug) logger.warn('[oauth] refresh failed, falling back to full auth:', err?.message)
223
+ }
224
+ }
225
+ }
226
+ return await this.runAuthCodeFlow()
227
+ }
228
+
229
+ async refresh(refresh_token) {
230
+ const body = new URLSearchParams({
231
+ grant_type: 'refresh_token',
232
+ refresh_token,
233
+ client_id: this.clientId,
234
+ })
235
+ const resp = await fetch(this.tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body })
236
+ if (!resp.ok) throw new Error(`OAuth refresh failed: ${resp.status}`)
237
+ const json = await resp.json()
238
+ this.persistTokens(json)
239
+ if (this.debug) logger.debug('[oauth] refresh ok; expires_in:', json.expires_in)
240
+ return json.access_token
241
+ }
242
+
243
+ /** Full PKCE flow with loopback listener on redirectUri. */
244
+ async runAuthCodeFlow() {
245
+ const { verifier, challenge } = this.generatePkce()
246
+ const state = randomBytes(16).toString('hex')
247
+
248
+ // Start a tiny loopback server to capture the code
249
+ const { server, codePromise } = this.listenForCode(state)
250
+
251
+ const authUrl = new URL(this.authorizationUrl)
252
+ authUrl.searchParams.set('response_type', 'code')
253
+ authUrl.searchParams.set('client_id', this.clientId)
254
+ authUrl.searchParams.set('redirect_uri', this.redirectUri.href)
255
+ authUrl.searchParams.set('scope', this.scopes.join(' '))
256
+ authUrl.searchParams.set('code_challenge', challenge)
257
+ authUrl.searchParams.set('code_challenge_method', 'S256')
258
+ authUrl.searchParams.set('state', state)
259
+
260
+ if (this.debug) logger.debug('[oauth] opening authorize URL:', authUrl.href)
261
+ //这里只需要打开授权地址,随后本地回调服务器会接收授权码并继续 PKCE 流程获取访问令牌
262
+ openInBrowser(authUrl)
263
+
264
+ // timeout guard to avoid hanging forever
265
+ const timeout = new Promise((_, rej) => setTimeout(() => rej(new Error('OAuth authorize timed out')), this.authTimeoutMs))
266
+ const { code, recvState } = await Promise.race([codePromise, timeout])
267
+
268
+ server.close()
269
+ if (recvState !== state) throw new Error('OAuth state mismatch')
270
+
271
+ const body = new URLSearchParams({
272
+ grant_type: 'authorization_code',
273
+ code,
274
+ redirect_uri: this.redirectUri.href,
275
+ client_id: this.clientId,
276
+ code_verifier: verifier,
277
+ })
278
+
279
+ //这里获取失败
280
+ const resp = await fetch(this.tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body })
281
+ if (!resp.ok) {
282
+ logger.error('[oauth] token exchange failed url:', this.tokenUrl + "");
283
+ logger.error('[oauth] token exchange failed body:', body + "");
284
+ logger.error('[oauth] token exchange failed response:', await resp.text());
285
+ throw new Error(`OAuth token exchange failed: ${resp.status}`);
286
+ }
287
+ const json = await resp.json()
288
+ this.persistTokens(json)
289
+ if (this.debug) logger.debug('[oauth] auth ok; expires_in:', json.expires_in)
290
+ return json.access_token
291
+ }
292
+
293
+ generatePkce() {
294
+ const verifier = randomBytes(32).toString('base64url')
295
+ const challenge = createHash('sha256').update(verifier).digest('base64url')
296
+ return { verifier, challenge }
297
+ }
298
+
299
+ listenForCode(expectedState) {
300
+ const url = new URL(this.redirectUri)
301
+ const port = Number(url.port || 80)
302
+ const pathname = url.pathname
303
+
304
+ let resolve, reject
305
+ const codePromise = new Promise((res, rej) => { resolve = res; reject = rej })
306
+
307
+ const server = createServer((req, res) => {
308
+ try {
309
+ if (req.method !== 'GET') { res.statusCode = 405; res.end('Method Not Allowed'); return }
310
+ const reqUrl = new URL(req.url, `${url.protocol}//${url.host}`)
311
+ if (reqUrl.pathname !== pathname) { res.statusCode = 404; res.end('Not Found'); return }
312
+ const code = reqUrl.searchParams.get('code')
313
+ const state = reqUrl.searchParams.get('state')
314
+
315
+ logger.debug('[oauth] received code:', code);
316
+ logger.debug('[oauth] received state:', state);
317
+
318
+ if (!code) { res.statusCode = 400; res.end('Missing code'); return }
319
+ res.statusCode = 200
320
+ res.end('<html><body>Authentication complete. You may close this window.<script>window.close()</script></body></html>');
321
+ resolve({ code, recvState: state })
322
+ } catch (err) {
323
+ reject(err)
324
+ }
325
+ })
326
+ server.listen(port, '127.0.0.1')
327
+ return { server, codePromise }
328
+ }
329
+
330
+ persistTokens(json) {
331
+ const expires_at = json.expires_in ? Date.now() + (json.expires_in * 1000) : undefined
332
+ const toStore = { ...json, expires_at }
333
+ this.store.set(this.storeKey, toStore)
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Minimal Streamable HTTP client transport (JSON-RPC 2.0 over HTTP with sessions)
339
+ *
340
+ * Server must support MCP Streamable HTTP endpoint. This transport manages:
341
+ * - Session header (Mcp-Session-Id) from the init response
342
+ * - OAuth bearer token acquisition/refresh via OAuthManager when provided
343
+ */
344
+ export class StreamableHTTPClientTransport {
345
+ constructor(baseUrl, { oauth } = {}) {
346
+ this.baseUrl = new URL(baseUrl)
347
+ this.oauth = oauth ? new OAuthManager(oauth) : null
348
+ this.sessionId = null
349
+ this.seq = 0
350
+ }
351
+
352
+ // 在文件顶部或类里工具函数处加上:
353
+ async readJSONorSSE(resp) {
354
+ const ct = (resp.headers.get('content-type') || '').toLowerCase()
355
+ if (ct.startsWith('application/json')) {
356
+ return await resp.json()
357
+ }
358
+ if (!ct.startsWith('text/event-stream')) {
359
+ // 兜底:不是 JSON 也不是 SSE,就按文本丢错
360
+ const text = await resp.text().catch(() => '')
361
+ logger.debug(`Unsupported content-type: ${ct}. Body: ${text}`);
362
+ return {result: {}, jsonrpc: '2.0'}
363
+ //throw new Error(`Unsupported content-type: ${ct}. Body: ${text}`)
364
+ }
365
+
366
+ // === 解析 SSE ===
367
+ const reader = resp.body.getReader()
368
+ const decoder = new TextDecoder()
369
+ let buffer = ''
370
+ let lastJson = null
371
+
372
+ const flushEvent = (evt) => {
373
+ // evt 可能包含多行 data:,把它们拼起来
374
+ const payload = (evt.data || []).join('\n')
375
+ if (!payload) return
376
+ try {
377
+ const obj = JSON.parse(payload)
378
+ // 只关心 JSON-RPC 完整消息;中间进度事件可按需处理
379
+ if (obj?.jsonrpc === '2.0' && (obj.result || obj.error || obj.id !== undefined)) {
380
+ lastJson = obj
381
+ }
382
+ } catch {
383
+ // 不是 JSON 的心跳/注释,忽略
384
+ }
385
+ }
386
+
387
+ let evt = { event: null, data: [] } // 当前事件累积器
388
+
389
+ while (true) {
390
+ const { done, value } = await reader.read()
391
+ if (done) break
392
+ buffer += decoder.decode(value, { stream: true })
393
+
394
+ // SSE 以 \n 分隔;空行表示一个事件结束
395
+ let idx
396
+ while ((idx = buffer.indexOf('\n')) >= 0) {
397
+ const lineRaw = buffer.slice(0, idx)
398
+ buffer = buffer.slice(idx + 1)
399
+ const line = lineRaw.replace(/\r$/, '')
400
+
401
+ if (line === '') { // 事件结束
402
+ flushEvent(evt)
403
+ evt = { event: null, data: [] }
404
+ continue
405
+ }
406
+ if (line.startsWith(':')) { // 注释/心跳
407
+ continue
408
+ }
409
+ if (line.startsWith('event:')) {
410
+ evt.event = line.slice(6).trim()
411
+ continue
412
+ }
413
+ if (line.startsWith('data:')) {
414
+ evt.data.push(line.slice(5).trimStart())
415
+ continue
416
+ }
417
+ // 可选:处理 id:/retry: 等字段,这里用不到
418
+ }
419
+ }
420
+
421
+ if (!lastJson) {
422
+ throw new Error('SSE stream ended without a JSON-RPC message')
423
+ }
424
+ return lastJson
425
+ }
426
+
427
+
428
+
429
+ /** Low-level send of a JSON-RPC request. */
430
+ async send(method, params) {
431
+ const id = ++this.seq
432
+ const body = { jsonrpc: '2.0', id, method, params }
433
+ //不需要 id 并且没有返回
434
+ if(method == "notifications/initialized"){
435
+ delete body.id;
436
+ }
437
+
438
+ const headers = { 'Content-Type': 'application/json' , 'Accept': 'application/json, text/event-stream' }
439
+ if (this.sessionId) headers['Mcp-Session-Id'] = this.sessionId
440
+
441
+ // Try attach OAuth if configured
442
+ if (this.oauth) {
443
+ const token = await this.oauth.getAccessToken().catch((e) => {
444
+ logger.warn('[oauth] getAccessToken failed:', e?.message)
445
+ return null
446
+ })
447
+ if (token) headers['Authorization'] = `Bearer ${token}`
448
+ }
449
+
450
+ //logger.debug('[oauth] sending request:', JSON.stringify({ method, params, headers , body }, null, 2))
451
+
452
+ let resp = await fetch(this.baseUrl, { method: 'POST', headers, body: JSON.stringify(body) })
453
+
454
+ // If unauthorized and OAuth is configured, try once to re-auth then retry
455
+ if (resp.status === 401 && this.oauth) {
456
+ const token = await this.oauth.runAuthCodeFlow().catch((e) => {
457
+ logger.warn('[oauth] re-auth failed:', e?.message)
458
+ return null
459
+ })
460
+
461
+ if (!token) {
462
+ const text = await resp.text().catch(() => '')
463
+ throw new Error(`HTTP 401 Unauthorized. Body: ${text}`)
464
+ }
465
+ headers['Authorization'] = `Bearer ${token}`
466
+ resp = await fetch(this.baseUrl, { method: 'POST', headers, body: JSON.stringify(body) })
467
+ }
468
+
469
+ if (!resp.ok) {
470
+ const text = await resp.text().catch(() => '')
471
+ throw new Error(`HTTP ${resp.status} ${resp.statusText} ${text}`)
472
+ }
473
+
474
+ // Capture session id if provided
475
+ const sid = resp.headers.get('Mcp-Session-Id')
476
+ if (sid) this.sessionId = sid
477
+
478
+ // 这里返回可能是 text/event-stream 流式返回
479
+
480
+ //logger.debug('[oauth] token exchange response:', await resp.clone().text());
481
+ //const json = await resp.json()
482
+ const json = await this.readJSONorSSE(resp);
483
+
484
+ //logger.debug('[oauth] token exchange response:', json);
485
+ if (json.error) {
486
+ const e = new Error(json.error.message || 'RPC Error')
487
+ e.code = json.error.code
488
+ e.data = json.error.data
489
+ throw e
490
+ }
491
+ return json.result
492
+ }
493
+ }
494
+
495
+ /**
496
+ * High-level MCP Client (SDK-compatible surface)
497
+ */
498
+ export class Client {
499
+ constructor({ name, version } = {}) {
500
+ this.name = name || 'mcp-client-js'
501
+ this.version = version || '0.0.0'
502
+ this.connected = false
503
+ this.transport = null
504
+ }
505
+
506
+ async connect(transport) {
507
+ this.transport = transport
508
+ // Initialize per MCP spec
509
+ const init = await this.transport.send('initialize', {
510
+ protocolVersion: '2025-05-15', // pick a modern version; adjust if your server differs
511
+ capabilities: {
512
+ prompts: {}, resources: {}, tools: {}, sampling: {},
513
+ },
514
+ clientInfo: { name: this.name, version: this.version }
515
+ })
516
+
517
+ this._initializeInfo = init;
518
+
519
+ // Acknowledge ready
520
+ await this.transport.send('notifications/initialized', {})
521
+ this.connected = true
522
+ this.serverInfo = init?.serverInfo
523
+ this.capabilities = init?.capabilities
524
+ return init
525
+ }
526
+
527
+ // === Prompts ===
528
+ async listPrompts() {
529
+ return await this.transport.send('prompts/list', {})
530
+ }
531
+ async getPrompt({ name, arguments: args = {} }) {
532
+ return await this.transport.send('prompts/get', { name, arguments: args })
533
+ }
534
+
535
+ // === Resources ===
536
+ async listResources() {
537
+ return await this.transport.send('resources/list', {})
538
+ }
539
+ async readResource({ uri }) {
540
+ return await this.transport.send('resources/read', { uri })
541
+ }
542
+
543
+ // === Tools ===
544
+ async listTools() {
545
+ return await this.transport.send('tools/list', {})
546
+ }
547
+ async callTool({ name, arguments: args = {} }) {
548
+ return await this.transport.send('tools/call', { name, arguments: args })
549
+ }
550
+
551
+ // === Convenience ===
552
+ get isConnected() { return this.connected }
553
+ }
554
+
555
+ // Optional: default export
556
+ export default { Client, StreamableHTTPClientTransport }
@@ -0,0 +1,118 @@
1
+ import net from 'node:net';
2
+ import { getPipePath } from '../untils.js';
3
+ import LogManager from '../logger-manager.js';
4
+ const logger = LogManager.getSystemLogger();
5
+
6
+ const PIPE_PATH = await getPipePath();
7
+ logger.debug("JsonRpcClient PIPE_PATH:" + PIPE_PATH);
8
+
9
+ /**
10
+ * 使用文件协议通信的 JSON-RPC 客户端
11
+ */
12
+ export default class JsonRpcClient {
13
+ constructor() {
14
+ this.socket = net.createConnection(PIPE_PATH);
15
+ this.nextId = 1;
16
+ this.pending = new Map();
17
+ this.connectionError = null;
18
+ this.connected = false;
19
+ this.connectionTimeout = 5000; // 5秒连接超时
20
+ let buf = '';
21
+
22
+ // 连接成功
23
+ this.socket.on('connect', () => {
24
+ logger.info('JSON-RPC client connected to', PIPE_PATH);
25
+ this.connected = true;
26
+ this.connectionError = null;
27
+ });
28
+
29
+ // 连接错误
30
+ this.socket.on('error', (e) => {
31
+ logger.error('JSON-RPC client connection error:', e.message);
32
+ this.connectionError = e;
33
+ this.connected = false;
34
+ for (const [, p] of this.pending) p.reject(e);
35
+ this.pending.clear();
36
+ });
37
+
38
+ // 连接关闭
39
+ this.socket.on('close', () => {
40
+ logger.info('JSON-RPC client connection closed');
41
+ let con = this.connected;
42
+ this.connected = false;
43
+ if(con){
44
+ this.connectionError = new Error('Connection closed');
45
+ //如果根本没有链接过,应该是链接失败
46
+ }else{
47
+ this.connectionError = new Error('Connection closed ,请检查服务是否启动');
48
+ }
49
+
50
+ });
51
+
52
+ // 数据接收处理
53
+ this.socket.on('data', (chunk) => {
54
+ buf += chunk;
55
+ for (let i = buf.indexOf('\n'); i >= 0; i = buf.indexOf('\n')) {
56
+ const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1);
57
+ if (!line) continue;
58
+ let msg; try { msg = JSON.parse(line); } catch { continue; }
59
+ const p = this.pending.get(msg.id);
60
+ if (p) {
61
+ this.pending.delete(msg.id);
62
+ msg.error ? p.reject(new Error(msg.error.message)) : p.resolve(msg.result);
63
+ }
64
+ }
65
+ });
66
+
67
+ }
68
+
69
+ // 等待连接建立或超时
70
+ async waitForConnection() {
71
+ if (this.connected) return;
72
+ if (this.connectionError) throw this.connectionError;
73
+
74
+ return new Promise((resolve, reject) => {
75
+ const timeout = setTimeout(() => {
76
+ reject(new Error(`Connection timeout: Server not responding at ${PIPE_PATH}`));
77
+ }, this.connectionTimeout);
78
+
79
+ this.socket.once('connect', () => {
80
+ clearTimeout(timeout);
81
+ resolve();
82
+ });
83
+
84
+ this.socket.once('error', (err) => {
85
+ clearTimeout(timeout);
86
+ reject(new Error(`Connection failed: ${err.message}`));
87
+ });
88
+ });
89
+ }
90
+
91
+ async call(method, params) {
92
+ // 检查连接状态
93
+ await this.waitForConnection();
94
+
95
+ const id = this.nextId++;
96
+ const req = { jsonrpc: '2.0', id, method, params };
97
+ return new Promise((resolve, reject) => {
98
+ this.pending.set(id, { resolve, reject });
99
+ this.socket.write(JSON.stringify(req) + '\n');
100
+ });
101
+ }
102
+ }
103
+
104
+ /**
105
+ // demo
106
+ (async () => {
107
+ const cli = new JsonRpcClient(PIPE_PATH);
108
+ try {
109
+ logger.debug('ping =>', await cli.call('ping'));
110
+ logger.debug('echo =>', await cli.call('echo', 'hello'));
111
+ logger.debug('add =>', await cli.call('add', [1, 2]));
112
+ } catch (e) {
113
+ logger.error('RPC error:', e.message);
114
+ } finally {
115
+ setTimeout(() => process.exit(0), 200);
116
+ }
117
+ })();
118
+ */