signalk-mcp-server
Version:
MCP server for SignalK marine data integration
169 lines (167 loc) • 5.59 kB
JavaScript
/**
* SDK Generator - Auto-generates TypeScript SDK from MCP tool definitions
*
* This generator converts MCP tool schemas into TypeScript functions that can be
* injected into V8 isolates, providing type-safe access to SignalK data.
*
* Benefits:
* - Keeps SDK in sync with MCP tools automatically
* - Provides TypeScript types and JSDoc comments
* - Handles parameters and options correctly
* - No manual SDK maintenance required
*/
/**
* Generate TypeScript SDK code from MCP tool definitions
*
* @param tools - Array of MCP tool definitions
* @returns TypeScript code as string
*
* @example
* const tools = [
* {
* name: 'get_vessel_state',
* description: 'Get current vessel navigation data',
* inputSchema: { type: 'object', properties: {} }
* }
* ];
* const sdk = generateSDK(tools);
* // Returns TypeScript function definitions
*/
export function generateSDK(tools) {
const functions = tools.map(tool => generateFunction(tool));
return `
/**
* Auto-generated SignalK SDK
* Generated from MCP tool definitions
*
* Usage in agent code:
* const vessel = await getVesselState();
* const targets = await getAisTargets({ page: 1, pageSize: 10 });
*/
${functions.join('\n\n')}
`.trim();
}
/**
* Generate a single TypeScript function from MCP tool definition
*
* @param tool - MCP tool definition
* @param mode - 'typescript' for .ts files, 'javascript' for isolates
* @returns Function code
*/
function generateFunction(tool, mode = 'typescript') {
const functionName = toCamelCase(tool.name);
const hasParams = tool.inputSchema.properties && Object.keys(tool.inputSchema.properties).length > 0;
// Generate JSDoc comment
const jsdoc = generateJSDoc(tool);
// Generate function signature based on mode
let params;
if (mode === 'javascript') {
// JavaScript: no types, just parameter name
params = hasParams ? 'options' : '';
}
else {
// TypeScript: with types
const paramType = hasParams ? generateParamType(tool.inputSchema) : '';
params = hasParams ? `options${isRequired(tool) ? '' : '?'}: ${paramType}` : '';
}
// Generate function body
const callArgs = hasParams ? 'options' : 'undefined';
return `${jsdoc}
async function ${functionName}(${params}) {
const result = await signalk.${functionName}(${callArgs});
return result;
}`;
}
/**
* Generate TypeScript parameter type from JSON schema
*/
function generateParamType(schema) {
if (!schema.properties || Object.keys(schema.properties).length === 0) {
return '{}';
}
const props = Object.entries(schema.properties).map(([key, prop]) => {
const optional = !schema.required?.includes(key);
const type = jsonSchemaTypeToTS(prop.type);
return ` ${key}${optional ? '?' : ''}: ${type};`;
});
return `{\n${props.join('\n')}\n}`;
}
/**
* Convert JSON schema type to TypeScript type
*/
function jsonSchemaTypeToTS(type) {
const typeMap = {
'string': 'string',
'number': 'number',
'integer': 'number',
'boolean': 'boolean',
'object': 'any',
'array': 'any[]',
};
return typeMap[type] || 'any';
}
/**
* Generate JSDoc comment from tool definition
*/
function generateJSDoc(tool) {
const lines = ['/**', ` * ${tool.description}`];
// Add parameter descriptions
if (tool.inputSchema.properties) {
lines.push(' *');
for (const [key, prop] of Object.entries(tool.inputSchema.properties)) {
const propAny = prop;
if (propAny.description) {
const optional = !tool.inputSchema.required?.includes(key);
lines.push(` * @param ${optional ? '[' : ''}options.${key}${optional ? ']' : ''} - ${propAny.description}`);
}
}
}
lines.push(' */');
return lines.join('\n');
}
/**
* Convert snake_case to camelCase
*/
function toCamelCase(str) {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
/**
* Check if tool has required parameters
*/
function isRequired(tool) {
return tool.inputSchema.required && tool.inputSchema.required.length > 0 || false;
}
/**
* Generate SDK code and inject into isolate context
*
* This function generates the SDK code and prepares it for injection
* into a V8 isolate context along with the signalk binding.
*
* @param tools - MCP tool definitions
* @returns Object with SDK code and injection helper
*/
export function prepareSDKForIsolate(tools) {
// Generate TypeScript SDK for .ts files (with types and exports)
const tsCode = generateSDK(tools);
const exportedCode = tsCode.replace(/^async function /gm, 'export async function ');
// Generate JavaScript SDK for isolate (no types, no exports)
const jsFunctions = tools.map(tool => generateFunction(tool, 'javascript'));
const jsCode = jsFunctions.join('\n\n');
// Dynamically generate global assignments for available functions
const globalAssignments = tools.map(tool => {
const functionName = toCamelCase(tool.name);
return `globalThis.${functionName} = ${functionName};`;
}).join('\n');
// Wrapper code to execute in isolate (pure JavaScript)
const wrapperCode = `
// SDK Functions (JavaScript - no TypeScript types)
${jsCode}
// Make functions available globally in isolate
${globalAssignments}
`.trim();
return {
code: exportedCode, // For TypeScript files
wrapperCode, // For isolate injection (pure JS)
};
}
//# sourceMappingURL=generator.js.map