/**
* HTTP 客戶端 - 轉發請求到 Cloud Run Backend
*/
import type { ToolRequest, ToolResponse } from '@mcp-internal/shared'
export interface HttpClientConfig {
baseUrl: string
token: string
timeout?: number
}
export class HttpClient {
private baseUrl: string
private token: string
private timeout: number
constructor(config: HttpClientConfig) {
this.baseUrl = config.baseUrl.replace(/\/$/, '')
this.token = config.token
this.timeout = config.timeout || 60000 // 60 秒預設
}
/**
* 執行工具
*/
async executeTool(toolName: string, args: Record<string, unknown>): Promise<ToolResponse> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
try {
const request: ToolRequest = { arguments: args }
const response = await fetch(`${this.baseUrl}/api/v1/tools/${toolName}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`,
},
body: JSON.stringify(request),
signal: controller.signal,
})
const data = await response.json() as ToolResponse
return data
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return {
success: false,
error: {
code: 'TIMEOUT',
message: '請求超時',
},
}
}
return {
success: false,
error: {
code: 'NETWORK_ERROR',
message: error instanceof Error ? error.message : String(error),
},
}
} finally {
clearTimeout(timeoutId)
}
}
/**
* 取得可用工具列表
*/
async listTools(): Promise<Array<{
name: string
description: string
inputSchema: any
}>> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000)
try {
const response = await fetch(`${this.baseUrl}/api/v1/tools`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.token}`,
},
signal: controller.signal,
})
const data = await response.json() as any
if (data.success && data.result?.tools) {
return data.result.tools
}
throw new Error(data.error?.message || '無法取得工具列表')
} finally {
clearTimeout(timeoutId)
}
}
/**
* 健康檢查
*/
async healthCheck(): Promise<boolean> {
try {
const response = await fetch(`${this.baseUrl}/health`, {
method: 'GET',
})
return response.ok
} catch {
return false
}
}
}
let httpClient: HttpClient | null = null
export function getHttpClient(): HttpClient {
if (!httpClient) {
const baseUrl = process.env.CLOUD_RUN_URL
const fsuid = process.env.FSUID
if (!baseUrl) {
throw new Error('CLOUD_RUN_URL 環境變數未設定')
}
if (!fsuid) {
throw new Error('FSUID 環境變數未設定')
}
httpClient = new HttpClient({ baseUrl, token: fsuid })
}
return httpClient
}