/** * MCP Web Extension * Provides Model Context Protocol functionality for web platform * Uses official MCP TypeScript SDK with proper session handling */ import { MCPExtension, MCPTool, MCPToolCallResult } from '@janhq/core' import { getSharedAuthService, JanAuthService } from '../shared' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { JanMCPOAuthProvider } from './oauth-provider' // JAN_API_BASE is defined in vite.config.ts (defaults to 'https://api-dev.jan.ai/jan/v1') declare const JAN_API_BASE: string export default class MCPExtensionWeb extends MCPExtension { private mcpEndpoint = '/mcp' private tools: MCPTool[] = [] private initialized = false private authService: JanAuthService private mcpClient: Client | null = null private oauthProvider: JanMCPOAuthProvider constructor( url: string, name: string, productName?: string, active?: boolean, description?: string, version?: string ) { super(url, name, productName, active, description, version) this.authService = getSharedAuthService() this.oauthProvider = new JanMCPOAuthProvider(this.authService) } async onLoad(): Promise { try { // Initialize MCP client with OAuth await this.initializeMCPClient() // Then fetch tools await this.initializeTools() } catch (error) { console.warn('Failed to initialize MCP extension:', error) this.tools = [] } } async onUnload(): Promise { this.tools = [] this.initialized = false // Close MCP client if (this.mcpClient) { try { await this.mcpClient.close() } catch (error) { console.warn('Error closing MCP client:', error) } this.mcpClient = null } } private async initializeMCPClient(): Promise { try { // Close existing client if any if (this.mcpClient) { try { await this.mcpClient.close() } catch (error) { // Ignore close errors } this.mcpClient = null } // Create transport with OAuth provider (handles token refresh automatically) const transport = new StreamableHTTPClientTransport( new URL(`${JAN_API_BASE}${this.mcpEndpoint}`), { authProvider: this.oauthProvider // No sessionId needed - server will generate one automatically } ) // Create MCP client this.mcpClient = new Client( { name: 'jan-web-client', version: '1.0.0' }, { capabilities: { tools: {}, resources: {}, prompts: {}, logging: {} } } ) // Connect to MCP server (OAuth provider handles auth automatically) await this.mcpClient.connect(transport) console.log('MCP client connected successfully, session ID:', transport.sessionId) } catch (error) { console.error('Failed to initialize MCP client:', error) throw error } } private async initializeTools(): Promise { if (this.initialized || !this.mcpClient) { return } try { // Use MCP SDK to list tools const result = await this.mcpClient.listTools() console.log('MCP tools/list response:', result) if (result.tools && Array.isArray(result.tools)) { this.tools = result.tools.map((tool) => ({ name: tool.name, description: tool.description || '', inputSchema: (tool.inputSchema || {}) as Record, server: 'Jan MCP Server' })) } else { console.warn('No tools found in MCP server response') this.tools = [] } this.initialized = true console.log(`Initialized MCP extension with ${this.tools.length} tools:`, this.tools.map(t => t.name)) } catch (error) { console.error('Failed to fetch MCP tools:', error) this.tools = [] this.initialized = false throw error } } async getTools(): Promise { if (!this.initialized) { await this.initializeTools() } return this.tools } async callTool(toolName: string, args: Record): Promise { if (!this.mcpClient) { return { error: 'MCP client not initialized', content: [{ type: 'text', text: 'MCP client not initialized' }] } } try { // Use MCP SDK to call tool (OAuth provider handles auth automatically) const result = await this.mcpClient.callTool({ name: toolName, arguments: args }) console.log(`MCP tool call result for ${toolName}:`, result) // Handle tool call result if (result.isError) { const errorText = Array.isArray(result.content) && result.content.length > 0 ? (result.content[0].type === 'text' ? (result.content[0] as any).text : 'Tool call failed') : 'Tool call failed' return { error: errorText, content: [{ type: 'text', text: errorText }] } } // Convert MCP content to Jan's format const content = Array.isArray(result.content) ? result.content.map(item => { if (item.type === 'text') { return { type: 'text' as const, text: (item as any).text } } else { // For non-text types, convert to text representation return { type: 'text' as const, text: JSON.stringify(item) } } }) : [{ type: 'text' as const, text: 'No content returned' }] return { error: '', content } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) console.error(`Failed to call MCP tool ${toolName}:`, error) return { error: errorMessage, content: [{ type: 'text', text: errorMessage }] } } } async isHealthy(): Promise { if (!this.mcpClient) { return false } try { // Try to list tools as health check (OAuth provider handles auth) await this.mcpClient.listTools() return true } catch (error) { console.warn('MCP health check failed:', error) return false } } async getConnectedServers(): Promise { // Return servers based on MCP client connection status return this.mcpClient && this.initialized ? ['Jan MCP Server'] : [] } async refreshTools(): Promise { this.initialized = false try { await this.initializeTools() } catch (error) { console.error('Failed to refresh tools:', error) throw error } } }