diff --git a/PROGRAMMATIC_API.md b/PROGRAMMATIC_API.md new file mode 100644 index 0000000..33517cf --- /dev/null +++ b/PROGRAMMATIC_API.md @@ -0,0 +1,137 @@ +# OpenAPI to MCP Generator - Programmatic API + +This document describes how to use the OpenAPI to MCP Generator programmatically in your Node.js applications. + +## Installation + +```bash +npm install openapi-mcp-generator +``` + +## Usage + +The package exports a simple, focused API for extracting MCP tool definitions from OpenAPI specifications: + +```typescript +import { getToolsFromOpenApi } from 'openapi-mcp-generator'; +``` + +### `getToolsFromOpenApi(specPathOrUrl, options)` + +This function extracts an array of tools from an OpenAPI specification. + +**Parameters:** +- `specPathOrUrl`: Path to a local OpenAPI spec file or URL to a remote spec +- `options`: (Optional) Configuration options + +**Options:** +- `baseUrl`: Override the base URL in the OpenAPI spec +- `dereference`: Whether to resolve $refs (default: false) +- `excludeOperationIds`: Array of operation IDs to exclude from the results +- `filterFn`: Custom function to filter tools (receives tool, returns boolean) + +**Returns:** +- Promise that resolves to an array of McpToolDefinition objects + +**Example:** + +```typescript +import { getToolsFromOpenApi } from 'openapi-mcp-generator'; + +// Basic usage +const tools = await getToolsFromOpenApi('./petstore.json'); + +// With options +const filteredTools = await getToolsFromOpenApi('https://petstore3.swagger.io/api/v3/openapi.json', { + baseUrl: 'https://petstore3.swagger.io/api/v3', + dereference: true, + excludeOperationIds: ['addPet', 'updatePet'], + filterFn: (tool) => tool.method.toLowerCase() === 'get' +}); + +// Process the results +for (const tool of filteredTools) { + console.log(`Tool: ${tool.name}`); + console.log(` Description: ${tool.description}`); + console.log(` Method: ${tool.method.toUpperCase()} ${tool.pathTemplate}`); + console.log(` OperationId: ${tool.operationId}`); +} +``` + +## Tool Definition Structure + +Each tool definition (`McpToolDefinition`) has the following properties: + +```typescript +interface McpToolDefinition { + /** Name of the tool, must be unique */ + name: string; + + /** Human-readable description of the tool */ + description: string; + + /** JSON Schema that defines the input parameters */ + inputSchema: JSONSchema7 | boolean; + + /** HTTP method for the operation (get, post, etc.) */ + method: string; + + /** URL path template with parameter placeholders */ + pathTemplate: string; + + /** OpenAPI parameter objects for this operation */ + parameters: OpenAPIV3.ParameterObject[]; + + /** Parameter names and locations for execution */ + executionParameters: { name: string; in: string }[]; + + /** Content type for request body, if applicable */ + requestBodyContentType?: string; + + /** Security requirements for this operation */ + securityRequirements: OpenAPIV3.SecurityRequirementObject[]; + + /** Original operation ID from the OpenAPI spec */ + operationId: string; + + /** Base URL for the API (if available) */ + baseUrl?: string; +} +``` + +## Advanced Filtering Examples + +### Filter by HTTP Method + +```typescript +const getTools = await getToolsFromOpenApi(specUrl, { + filterFn: (tool) => tool.method.toLowerCase() === 'get' +}); +``` + +### Filter by Security Requirements + +```typescript +const secureTools = await getToolsFromOpenApi(specUrl, { + filterFn: (tool) => tool.securityRequirements.length > 0 +}); +``` + +### Filter by Path Pattern + +```typescript +const userTools = await getToolsFromOpenApi(specUrl, { + filterFn: (tool) => tool.pathTemplate.includes('/user') +}); +``` + +### Combining Multiple Filters + +```typescript +const safeUserTools = await getToolsFromOpenApi(specUrl, { + excludeOperationIds: ['deleteUser', 'updateUser'], + filterFn: (tool) => + tool.pathTemplate.includes('/user') && + tool.method.toLowerCase() === 'get' +}); +``` \ No newline at end of file diff --git a/README.md b/README.md index f24b516..0b8093a 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,28 @@ openapi-mcp-generator --input path/to/openapi.json --output path/to/output/dir - | `--transport` | `-t` | Transport mode: `"stdio"` (default), `"web"`, or `"streamable-http"` | `"stdio"` | | `--port` | `-p` | Port for web-based transports | `3000` | | `--force` | | Overwrite existing files in the output directory without confirmation | `false` | + +## 📦 Programmatic API + +You can also use this package programmatically in your Node.js applications: + +```javascript +import { getToolsFromOpenApi } from 'openapi-mcp-generator'; + +// Extract MCP tool definitions from an OpenAPI spec +const tools = await getToolsFromOpenApi('./petstore.json'); + +// With options +const filteredTools = await getToolsFromOpenApi('https://example.com/api-spec.json', { + baseUrl: 'https://api.example.com', + dereference: true, + excludeOperationIds: ['deletePet'], + filterFn: (tool) => tool.method.toLowerCase() === 'get' +}); +``` + +For full documentation of the programmatic API, see [PROGRAMMATIC_API.md](./PROGRAMMATIC_API.md). + --- ## 🧱 Project Structure diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..c884687 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,81 @@ +/** + * Programmatic API for OpenAPI to MCP Generator + * Allows direct usage from other Node.js applications + */ + +import SwaggerParser from '@apidevtools/swagger-parser'; +import { OpenAPIV3 } from 'openapi-types'; +import { extractToolsFromApi } from './parser/extract-tools.js'; +import { McpToolDefinition } from './types/index.js'; +import { determineBaseUrl } from './utils/url.js'; + +/** + * Options for generating the MCP tools + */ +export interface GetToolsOptions { + /** Optional base URL to override the one in the OpenAPI spec */ + baseUrl?: string; + + /** Whether to dereference the OpenAPI spec */ + dereference?: boolean; + + /** Array of operation IDs to exclude from the tools list */ + excludeOperationIds?: string[]; + + /** Optional filter function to exclude tools based on custom criteria */ + filterFn?: (tool: McpToolDefinition) => boolean; +} + +/** + * Get a list of tools from an OpenAPI specification + * + * @param specPathOrUrl Path or URL to the OpenAPI specification + * @param options Options for generating the tools + * @returns Promise that resolves to an array of tool definitions + */ +export async function getToolsFromOpenApi( + specPathOrUrl: string, + options: GetToolsOptions = {} +): Promise { + try { + // Parse the OpenAPI spec + const api = options.dereference + ? (await SwaggerParser.dereference(specPathOrUrl)) as OpenAPIV3.Document + : (await SwaggerParser.parse(specPathOrUrl)) as OpenAPIV3.Document; + + // Extract tools from the API + const allTools = extractToolsFromApi(api); + + // Add base URL to each tool + const baseUrl = determineBaseUrl(api, options.baseUrl); + + // Apply filters to exclude specified operationIds and custom filter function + let filteredTools = allTools; + + // Filter by excluded operation IDs if provided + if (options.excludeOperationIds && options.excludeOperationIds.length > 0) { + const excludeSet = new Set(options.excludeOperationIds); + filteredTools = filteredTools.filter(tool => !excludeSet.has(tool.operationId)); + } + + // Apply custom filter function if provided + if (options.filterFn) { + filteredTools = filteredTools.filter(options.filterFn); + } + + // Return the filtered tools with base URL added + return filteredTools.map(tool => ({ + ...tool, + baseUrl: baseUrl || '', + })); + } catch (error) { + // Provide more context for the error + if (error instanceof Error) { + throw new Error(`Failed to extract tools from OpenAPI: ${error.message}`); + } + throw error; + } +} + +// Export types for convenience +export { McpToolDefinition }; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a22d3a3..e33a374 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,9 @@ import { // Import types import { CliOptions, TransportType } from './types/index.js'; +// Export programmatic API +export { getToolsFromOpenApi, McpToolDefinition, GetToolsOptions } from './api.js'; + // Configure CLI const program = new Command(); @@ -69,18 +72,27 @@ program (val) => parseInt(val, 10) ) .option('--force', 'Overwrite existing files without prompting') - .version('2.0.0'); // Match package.json version + .version('3.0.0'); // Match package.json version -// Parse arguments explicitly from process.argv -program.parse(process.argv); +// Check if module is being run directly (not imported) +const isMainModule = process.argv[1] === new URL(import.meta.url).pathname; -// Retrieve the options AFTER parsing -const options = program.opts(); +if (isMainModule) { + // Parse arguments explicitly from process.argv + program.parse(process.argv); + + // Run with the parsed options + runGenerator(program.opts()) + .catch((error) => { + console.error('Unhandled error:', error); + process.exit(1); + }); +} /** * Main function to run the generator */ -async function main() { +async function runGenerator(options: CliOptions & { force?: boolean }) { // Use the parsed options directly const outputDir = options.output; const inputSpec = options.input; @@ -275,7 +287,5 @@ async function main() { } } -main().catch((error) => { - console.error('Unhandled error:', error); - process.exit(1); -}); +// Export the run function for programmatic usage +export { runGenerator as generateMcpServer }; \ No newline at end of file