Skip to main content
Glama
portel-dev

NCP - Natural Context Provider

by portel-dev
photon-adapter.ts7.59 kB
/** * Photon Adapter * * Adapts Photon classes to the InternalMCP interface * Handles schema extraction and tool registration */ import { InternalMCP, InternalTool, InternalToolResult } from './types.js'; import { PhotonMCP, SchemaExtractor } from '@portel/photon-core'; import { logger } from '../utils/logger.js'; import * as path from 'path'; /** * Adapter that converts Photon instances to InternalMCP interface */ export class PhotonAdapter implements InternalMCP { public readonly name: string; public readonly description: string; public tools: InternalTool[]; private instance: PhotonMCP; private mcpClass: typeof PhotonMCP; private sourceFilePath?: string; private constructor( mcpClass: typeof PhotonMCP, instance: PhotonMCP, sourceFilePath?: string ) { this.mcpClass = mcpClass; this.instance = instance; this.sourceFilePath = sourceFilePath; // Get MCP name from class (handle both Photon subclasses and plain classes) this.name = this.getMCPName(mcpClass); // Get description from class JSDoc or use default this.description = this.extractClassDescription() || `${this.name} Photon (built-in)`; // Tools will be initialized asynchronously this.tools = []; } /** * Get MCP name from class (supports both Photon subclasses and plain classes) */ private getMCPName(mcpClass: any): string { // Try to use Photon's method if available if (typeof mcpClass.getMCPName === 'function') { return mcpClass.getMCPName(); } // Fallback: implement convention for plain classes // Convert PascalCase to kebab-case (e.g., MyAwesomeMCP → my-awesome-mcp) return mcpClass.name .replace(/MCP$/, '') // Remove "MCP" suffix if present .replace(/([A-Z])/g, '-$1') .toLowerCase() .replace(/^-/, ''); // Remove leading dash } /** * Create and initialize a PhotonAdapter */ static async create( mcpClass: typeof PhotonMCP, instance: PhotonMCP, sourceFilePath?: string ): Promise<PhotonAdapter> { const adapter = new PhotonAdapter(mcpClass, instance, sourceFilePath); await adapter.initializeTools(); return adapter; } /** * Get tool methods from class (supports both Photon subclasses and plain classes) */ private getToolMethods(mcpClass: any): string[] { // Try to use Photon's method if available if (typeof mcpClass.getToolMethods === 'function') { return mcpClass.getToolMethods(); } // Fallback: implement convention for plain classes const prototype = mcpClass.prototype; const methods: string[] = []; Object.getOwnPropertyNames(prototype).forEach((name) => { // Skip constructor, private methods (starting with _), and lifecycle hooks if ( name !== 'constructor' && !name.startsWith('_') && name !== 'onInitialize' && name !== 'onShutdown' && typeof prototype[name] === 'function' ) { methods.push(name); } }); return methods; } /** * Initialize tools from class methods */ private async initializeTools(): Promise<void> { const methodNames = this.getToolMethods(this.mcpClass); // If source file available, extract schemas from source code if (this.sourceFilePath) { await this.extractSchemasFromSource(methodNames); } else { // Fallback: create basic tools without detailed schemas this.createBasicTools(methodNames); } } /** * Extract schemas from .schema.json or TypeScript source code */ private async extractSchemasFromSource(methodNames: string[]) { if (!this.sourceFilePath) return; try { let schemas: any[] = []; // First, try loading from pre-generated .schema.json file // This is used in packaged DXT where .ts files aren't included const schemaJsonPath = this.sourceFilePath.replace(/\.ts$/, '.schema.json'); try { const fs = await import('fs/promises'); const schemaContent = await fs.readFile(schemaJsonPath, 'utf-8'); schemas = JSON.parse(schemaContent); logger.debug(`Loaded ${schemas.length} schemas from ${path.basename(schemaJsonPath)}`); } catch (jsonError: any) { // .schema.json doesn't exist, try extracting from .ts source if (jsonError.code === 'ENOENT') { logger.debug(`No .schema.json file found, trying .ts source`); const extractor = new SchemaExtractor(); schemas = await extractor.extractFromFile(this.sourceFilePath); logger.debug(`Extracted ${schemas.length} schemas from ${path.basename(this.sourceFilePath)}`); } else { throw jsonError; } } // If no schemas found, use basic tools fallback if (schemas.length === 0) { logger.debug(`No schemas available for ${path.basename(this.sourceFilePath)}, using basic tools`); this.createBasicTools(methodNames); return; } // Create tools from schemas for (const schema of schemas) { if (methodNames.includes(schema.name)) { this.tools.push({ name: schema.name, description: schema.description, inputSchema: schema.inputSchema, }); } } logger.debug(`Created ${this.tools.length} tools with schemas`); } catch (error: any) { logger.warn(`Failed to load schemas: ${error.message}. Using basic tools.`); this.createBasicTools(methodNames); } } /** * Create basic tools without detailed schemas (fallback) */ private createBasicTools(methodNames: string[]) { for (const methodName of methodNames) { this.tools.push({ name: methodName, description: `${methodName} tool`, inputSchema: { type: 'object', properties: {}, }, }); } } /** * Extract class description from JSDoc comment */ private extractClassDescription(): string | null { // Try to get description from class metadata if available // This would require decorator or other mechanism return null; } /** * Execute a tool (supports both Photon subclasses and plain classes) */ async executeTool(toolName: string, parameters: any): Promise<InternalToolResult> { try { let result: any; // Check if instance has Photon's executeTool method if (typeof this.instance.executeTool === 'function') { result = await this.instance.executeTool(toolName, parameters); } else { // Plain class - call method directly const method = (this.instance as any)[toolName]; if (!method || typeof method !== 'function') { throw new Error(`Tool not found: ${toolName}`); } result = await method.call(this.instance, parameters); } // Normalize result to InternalToolResult format if (typeof result === 'string') { return { success: true, content: result, }; } else if (result && typeof result === 'object') { if ('success' in result && 'content' in result) { return result as InternalToolResult; } else { return { success: true, content: JSON.stringify(result, null, 2), }; } } else { return { success: true, content: String(result), }; } } catch (error: any) { return { success: false, error: error.message || 'Tool execution failed', }; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/portel-dev/ncp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server