@williamp29/project-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # OpenAPI MCP Server
2
+
3
+ A powerful Model Context Protocol (MCP) server that dynamically explores and interacts with any API defined by an OpenAPI specification.
4
+
5
+ ## For LLM Agents: How to use this MCP
6
+
7
+ This server provides a set of "meta-tools" that allow you to discover and interact with an API without needing individual tools for every endpoint.
8
+
9
+ ### Recommended Workflow:
10
+ 1. **Discovery**: Start by calling `get_tags` to understand the broad areas of the API.
11
+ 2. **Listing**: Use `get_tag_endpoints` or `get_all_endpoints` to see available actions for a specific topic.
12
+ 3. **Details**: Call `get_endpoint` for a specific path and method to see exactly what parameters and request body are required.
13
+ 4. **Execution**: Use `call_endpoint` to perform the actual API request.
14
+
15
+ ### Identity & Impersonation:
16
+ If the server is configured with an `identity` strategy, you can use `set_identity` to act on behalf of a specific user (e.g., passing a UID).
17
+
18
+ ---
19
+
20
+ ## Installation & Setup
21
+
22
+ ### 1. Build the project
23
+ ```bash
24
+ npm install
25
+ npm run build
26
+ ```
27
+
28
+ ### 2. Configure Environment Variables
29
+ Create a `.env` file (see `.env.example`):
30
+ - `PROJECT_MCP_API_BASE_URL`: The target API URL.
31
+ - `PROJECT_MCP_AUTH_TYPE`: `bearer`, `identity`, or `none`.
32
+ - `PROJECT_MCP_AUTH_IDENTIFIABLE`: (Optional) e.g., `UID:`.
33
+ - `PROJECT_MCP_AUTH_IDENTIFIER`: Default ID value.
34
+
35
+ ### 3. Usage in Cursor / Claude Desktop
36
+ Add a new MCP server with the following command:
37
+ ```bash
38
+ node /path/to/dist/cli.js
39
+ ```
40
+
41
+ Or if installed via npm:
42
+ ```bash
43
+ npx @williamp29/project-mcp-server
44
+ ```
45
+
46
+ ## Integration in Your Project
47
+
48
+ The recommended way to use this package is to install it in your project and create a custom entry point.
49
+
50
+ ### Step 1: Install
51
+ ```bash
52
+ npm install @williamp29/project-mcp-server
53
+ ```
54
+
55
+ ### Step 2: Create your MCP entry file
56
+ Create a file called `mcp-serve.js` (or `.ts`) in your project root:
57
+
58
+ ```javascript
59
+ // mcp-serve.js
60
+ import { MCPServer } from "@williamp29/project-mcp-server";
61
+ import { AuthStrategy, GlobalAuthContext } from "@williamp29/project-mcp-server/api-explorer";
62
+
63
+ // Your custom authentication logic
64
+ class MyAuth implements AuthStrategy {
65
+ name = "MyAuth";
66
+ async getHeaders() {
67
+ // Read from your own config, database, vault, etc.
68
+ return { "Authorization": `Bearer ${process.env.MY_API_TOKEN}` };
69
+ }
70
+ }
71
+
72
+ const authContext = new GlobalAuthContext(new MyAuth());
73
+ const server = new MCPServer("./openapi-spec.json", authContext);
74
+ server.start();
75
+ ```
76
+
77
+ ### Step 3: Add the npm script
78
+ In your project's `package.json`:
79
+ ```json
80
+ {
81
+ "scripts": {
82
+ "mcp:serve": "node mcp-serve.js"
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### Step 4: Configure Cursor
88
+ In your Cursor MCP settings (`mcp_settings.json`):
89
+ ```json
90
+ {
91
+ "my-api-mcp": {
92
+ "command": "npm",
93
+ "args": ["run", "mcp:serve"],
94
+ "cwd": "/absolute/path/to/your/project"
95
+ }
96
+ }
97
+ ```
98
+
99
+ Now Cursor will run your custom script whenever it needs to use the MCP server.
100
+
101
+ ---
102
+
103
+ ## Programmatic Usage
104
+
105
+ If you are using this as a library in your own Node.js project:
106
+
107
+ ### Basic Setup
108
+ ```typescript
109
+ import { MCPServer } from "@williamp29/project-mcp-server";
110
+
111
+ const server = new MCPServer("./openapi-spec.json");
112
+ server.start().catch(console.error);
113
+ ```
114
+
115
+ ### Custom Authentication Strategy
116
+ You can implement your own logic for fetching or rotating tokens:
117
+
118
+ ```typescript
119
+ import { MCPServer } from "@williamp29/project-mcp-server";
120
+ import { AuthStrategy, GlobalAuthContext } from "@williamp29/project-mcp-server/api-explorer";
121
+
122
+ class MyCustomAuth implements AuthStrategy {
123
+ name = "MyCustomAuth";
124
+ async getHeaders() {
125
+ const token = await fetchTokenFromVault();
126
+ return { "X-Custom-Auth": token };
127
+ }
128
+ }
129
+
130
+ const authContext = new GlobalAuthContext(new MyCustomAuth());
131
+ const server = new MCPServer("./spec.json", authContext);
132
+ server.start();
133
+ ```
134
+
135
+ ### Request Hooks
136
+ Add custom logic to every request (logging, tracing, extra headers):
137
+
138
+ ```typescript
139
+ import { addRequestHook } from "@williamp29/project-mcp-server/api-explorer";
140
+
141
+ addRequestHook((config) => {
142
+ console.error(`[API] ${config.method?.toUpperCase()} ${config.url}`);
143
+ config.headers["X-Request-ID"] = "mcp-123";
144
+ return config;
145
+ });
146
+ ```
147
+
148
+ ## Features
149
+ - **Dynamic Exploration**: Automatically parses Swagger/OpenAPI 3.0 specs.
150
+ - **Smart Execution**: Handles path parameters (e.g., `{id}`), query strings, and JSON bodies.
151
+ - **Flexible Auth**: Support for standard Bearer tokens and custom Identity headers.
152
+ - **Interceptors**: Easily extendable with request hooks for logging or custom headers.
153
+ - **Impersonation**: Built-in `set_identity` tool to switch users on the fly.
@@ -0,0 +1,39 @@
1
+ import { AuthContext } from "./auth/index.js";
2
+ export interface CallEndpointArgs {
3
+ method: string;
4
+ path: string;
5
+ parameters?: Record<string, any>;
6
+ body?: any;
7
+ }
8
+ /**
9
+ * Handles the actual HTTP communication with the API.
10
+ * Uses axios interceptors to dynamically inject authentication headers and run request hooks.
11
+ */
12
+ export declare class ApiExecutor {
13
+ private client;
14
+ private authContext;
15
+ /**
16
+ * @param baseURL - The target API base URL. Defaults to env.PROJECT_MCP_API_BASE_URL.
17
+ * @param authContext - The AuthContext managing the current authentication strategy.
18
+ */
19
+ constructor(baseURL?: string, authContext?: AuthContext);
20
+ /**
21
+ * Executes an API request based on the provided meta-tool arguments.
22
+ * Handles path parameter substitution and query parameter mapping.
23
+ * @param args - The method, path, and optional parameters/body.
24
+ */
25
+ callEndpoint(args: CallEndpointArgs): Promise<{
26
+ status: number;
27
+ statusText: string;
28
+ data: any;
29
+ error?: undefined;
30
+ message?: undefined;
31
+ } | {
32
+ error: boolean;
33
+ status: number | undefined;
34
+ statusText: string | undefined;
35
+ data: any;
36
+ message: string;
37
+ }>;
38
+ getAuthContext(): AuthContext;
39
+ }
@@ -0,0 +1,87 @@
1
+ import axios from "axios";
2
+ import dotenv from "dotenv";
3
+ import { createAuthContextFromEnv, runRequestHooks } from "./auth/index.js";
4
+ dotenv.config();
5
+ /**
6
+ * Handles the actual HTTP communication with the API.
7
+ * Uses axios interceptors to dynamically inject authentication headers and run request hooks.
8
+ */
9
+ export class ApiExecutor {
10
+ client;
11
+ authContext;
12
+ /**
13
+ * @param baseURL - The target API base URL. Defaults to env.PROJECT_MCP_API_BASE_URL.
14
+ * @param authContext - The AuthContext managing the current authentication strategy.
15
+ */
16
+ constructor(baseURL, authContext) {
17
+ const finalBaseURL = baseURL || process.env.PROJECT_MCP_API_BASE_URL || "http://localhost:5000";
18
+ this.authContext = authContext || createAuthContextFromEnv();
19
+ this.client = axios.create({
20
+ baseURL: finalBaseURL,
21
+ headers: {
22
+ "Content-Type": "application/json",
23
+ },
24
+ });
25
+ // Add interceptor for dynamic auth and hooks
26
+ this.client.interceptors.request.use(async (config) => {
27
+ // 1. Inject Dynamic Auth Headers
28
+ const authHeaders = await this.authContext.getHeaders();
29
+ // Log for debugging (stderr)
30
+ if (Object.keys(authHeaders).length > 0) {
31
+ console.error(`[ApiExecutor] Injecting auth headers from strategy: ${this.authContext.strategy.name}`);
32
+ }
33
+ Object.assign(config.headers, authHeaders);
34
+ // 2. Run Request Hooks
35
+ return await runRequestHooks(config);
36
+ });
37
+ }
38
+ /**
39
+ * Executes an API request based on the provided meta-tool arguments.
40
+ * Handles path parameter substitution and query parameter mapping.
41
+ * @param args - The method, path, and optional parameters/body.
42
+ */
43
+ async callEndpoint(args) {
44
+ const { method, path, parameters, body } = args;
45
+ let renderedPath = path;
46
+ const queryParams = {};
47
+ if (parameters) {
48
+ Object.entries(parameters).forEach(([name, value]) => {
49
+ const placeholder = `{${name}}`;
50
+ if (renderedPath.includes(placeholder)) {
51
+ renderedPath = renderedPath.replace(placeholder, String(value));
52
+ }
53
+ else {
54
+ queryParams[name] = value;
55
+ }
56
+ });
57
+ }
58
+ try {
59
+ const response = await this.client.request({
60
+ method: method.toUpperCase(),
61
+ url: renderedPath,
62
+ params: queryParams,
63
+ data: body,
64
+ });
65
+ return {
66
+ status: response.status,
67
+ statusText: response.statusText,
68
+ data: response.data,
69
+ };
70
+ }
71
+ catch (error) {
72
+ if (axios.isAxiosError(error)) {
73
+ return {
74
+ error: true,
75
+ status: error.response?.status,
76
+ statusText: error.response?.statusText,
77
+ data: error.response?.data,
78
+ message: error.message,
79
+ };
80
+ }
81
+ throw error;
82
+ }
83
+ }
84
+ getAuthContext() {
85
+ return this.authContext;
86
+ }
87
+ }
@@ -0,0 +1,16 @@
1
+ import { AuthContext, AuthStrategy } from "./types.js";
2
+ /**
3
+ * Standard implementation of AuthContext that wraps an AuthStrategy.
4
+ * Handles runtime identifier updates for identity-based strategies.
5
+ */
6
+ export declare class GlobalAuthContext implements AuthContext {
7
+ strategy: AuthStrategy;
8
+ /**
9
+ * @param strategy - The initial authentication strategy to use.
10
+ */
11
+ constructor(strategy: AuthStrategy);
12
+ setIdentifier(value: string): void;
13
+ getIdentifier(): string;
14
+ getHeaders(): Promise<Record<string, string>>;
15
+ }
16
+ export declare function createAuthContextFromEnv(): AuthContext;
@@ -0,0 +1,52 @@
1
+ import { BearerStrategy } from "./strategies/bearer-strategy.js";
2
+ import { IdentityStrategy } from "./strategies/identity-strategy.js";
3
+ import { NoAuthStrategy } from "./strategies/no-auth-strategy.js";
4
+ import dotenv from "dotenv";
5
+ dotenv.config();
6
+ /**
7
+ * Standard implementation of AuthContext that wraps an AuthStrategy.
8
+ * Handles runtime identifier updates for identity-based strategies.
9
+ */
10
+ export class GlobalAuthContext {
11
+ strategy;
12
+ /**
13
+ * @param strategy - The initial authentication strategy to use.
14
+ */
15
+ constructor(strategy) {
16
+ this.strategy = strategy;
17
+ }
18
+ setIdentifier(value) {
19
+ if (this.strategy instanceof IdentityStrategy) {
20
+ this.strategy.setIdentifier(value);
21
+ }
22
+ else {
23
+ console.error(`Current strategy ${this.strategy.name} does not support setting an identifier.`);
24
+ }
25
+ }
26
+ getIdentifier() {
27
+ if (this.strategy instanceof IdentityStrategy) {
28
+ return this.strategy.getIdentifier();
29
+ }
30
+ return "";
31
+ }
32
+ async getHeaders() {
33
+ return await this.strategy.getHeaders();
34
+ }
35
+ }
36
+ export function createAuthContextFromEnv() {
37
+ const authType = process.env.PROJECT_MCP_AUTH_TYPE || "bearer";
38
+ let strategy;
39
+ switch (authType.toLowerCase()) {
40
+ case "identity":
41
+ strategy = new IdentityStrategy(process.env.PROJECT_MCP_AUTH_HEADER_KEY || "Authorization", process.env.PROJECT_MCP_AUTH_IDENTIFIABLE || "UID:", process.env.PROJECT_MCP_AUTH_IDENTIFIER || "");
42
+ break;
43
+ case "bearer":
44
+ strategy = new BearerStrategy(process.env.PROJECT_MCP_API_BEARER_TOKEN || "");
45
+ break;
46
+ case "none":
47
+ default:
48
+ strategy = new NoAuthStrategy();
49
+ break;
50
+ }
51
+ return new GlobalAuthContext(strategy);
52
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./auth-context.js";
3
+ export * from "./request-hooks.js";
4
+ export * from "./strategies/bearer-strategy.js";
5
+ export * from "./strategies/identity-strategy.js";
6
+ export * from "./strategies/no-auth-strategy.js";
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./auth-context.js";
3
+ export * from "./request-hooks.js";
4
+ export * from "./strategies/bearer-strategy.js";
5
+ export * from "./strategies/identity-strategy.js";
6
+ export * from "./strategies/no-auth-strategy.js";
@@ -0,0 +1,16 @@
1
+ import { RequestHook } from "./types.js";
2
+ import { InternalAxiosRequestConfig } from "axios";
3
+ /**
4
+ * Registers a new hook to be run before every outgoing API request.
5
+ * Hooks can be used to log, trace, or modify the request configuration.
6
+ */
7
+ export declare function addRequestHook(hook: RequestHook): void;
8
+ /**
9
+ * Executes all registered request hooks in the order they were added.
10
+ * @param config - The axios request configuration.
11
+ */
12
+ export declare function runRequestHooks(config: InternalAxiosRequestConfig): Promise<InternalAxiosRequestConfig>;
13
+ /**
14
+ * Removes all registered request hooks. Useful for testing or resets.
15
+ */
16
+ export declare function clearRequestHooks(): void;
@@ -0,0 +1,25 @@
1
+ const hooks = [];
2
+ /**
3
+ * Registers a new hook to be run before every outgoing API request.
4
+ * Hooks can be used to log, trace, or modify the request configuration.
5
+ */
6
+ export function addRequestHook(hook) {
7
+ hooks.push(hook);
8
+ }
9
+ /**
10
+ * Executes all registered request hooks in the order they were added.
11
+ * @param config - The axios request configuration.
12
+ */
13
+ export async function runRequestHooks(config) {
14
+ let currentConfig = config;
15
+ for (const hook of hooks) {
16
+ currentConfig = await hook(currentConfig);
17
+ }
18
+ return currentConfig;
19
+ }
20
+ /**
21
+ * Removes all registered request hooks. Useful for testing or resets.
22
+ */
23
+ export function clearRequestHooks() {
24
+ hooks.length = 0;
25
+ }
@@ -0,0 +1,10 @@
1
+ import { AuthStrategy } from "../types.js";
2
+ /**
3
+ * Authentication strategy that uses a standard Authorization: Bearer <token> header.
4
+ */
5
+ export declare class BearerStrategy implements AuthStrategy {
6
+ private token;
7
+ name: string;
8
+ constructor(token: string);
9
+ getHeaders(): Record<string, string>;
10
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Authentication strategy that uses a standard Authorization: Bearer <token> header.
3
+ */
4
+ export class BearerStrategy {
5
+ token;
6
+ name = "Bearer";
7
+ constructor(token) {
8
+ this.token = token;
9
+ }
10
+ getHeaders() {
11
+ if (!this.token)
12
+ return {};
13
+ return {
14
+ Authorization: `Bearer ${this.token}`,
15
+ };
16
+ }
17
+ }
@@ -0,0 +1,15 @@
1
+ import { AuthStrategy } from "../types.js";
2
+ /**
3
+ * Highly flexible authentication strategy for custom headers and identifiers.
4
+ * Useful for scenarios like "Authorization: UID <identifier>" or "X-API-Key: <key>".
5
+ */
6
+ export declare class IdentityStrategy implements AuthStrategy {
7
+ private headerKey;
8
+ private identifiable;
9
+ name: string;
10
+ private currentIdentifier;
11
+ constructor(headerKey: string, identifiable: string, initialIdentifier: string);
12
+ setIdentifier(value: string): void;
13
+ getIdentifier(): string;
14
+ getHeaders(): Record<string, string>;
15
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Highly flexible authentication strategy for custom headers and identifiers.
3
+ * Useful for scenarios like "Authorization: UID <identifier>" or "X-API-Key: <key>".
4
+ */
5
+ export class IdentityStrategy {
6
+ headerKey;
7
+ identifiable;
8
+ name = "Identity";
9
+ currentIdentifier;
10
+ constructor(headerKey, identifiable, initialIdentifier) {
11
+ this.headerKey = headerKey;
12
+ this.identifiable = identifiable;
13
+ this.currentIdentifier = initialIdentifier;
14
+ }
15
+ setIdentifier(value) {
16
+ this.currentIdentifier = value;
17
+ }
18
+ getIdentifier() {
19
+ return this.currentIdentifier;
20
+ }
21
+ getHeaders() {
22
+ if (!this.currentIdentifier)
23
+ return {};
24
+ const value = this.identifiable
25
+ ? `${this.identifiable} ${this.currentIdentifier}`
26
+ : this.currentIdentifier;
27
+ return {
28
+ [this.headerKey]: value,
29
+ };
30
+ }
31
+ }
@@ -0,0 +1,8 @@
1
+ import { AuthStrategy } from "../types.js";
2
+ /**
3
+ * Strategy for APIs that do not require any authentication.
4
+ */
5
+ export declare class NoAuthStrategy implements AuthStrategy {
6
+ name: string;
7
+ getHeaders(): Record<string, string>;
8
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Strategy for APIs that do not require any authentication.
3
+ */
4
+ export class NoAuthStrategy {
5
+ name = "None";
6
+ getHeaders() {
7
+ return {};
8
+ }
9
+ }
@@ -0,0 +1,25 @@
1
+ import { InternalAxiosRequestConfig } from "axios";
2
+ /**
3
+ * Interface for authentication strategies that provide HTTP headers.
4
+ */
5
+ export interface AuthStrategy {
6
+ name: string;
7
+ /**
8
+ * Returns a dictionary of headers to be injected into the request.
9
+ */
10
+ getHeaders(): Record<string, string> | Promise<Record<string, string>>;
11
+ }
12
+ /**
13
+ * Manages the current authentication state and allows dynamic identifier updates.
14
+ */
15
+ export interface AuthContext {
16
+ strategy: AuthStrategy;
17
+ /**
18
+ * Updates the user identifier (e.g. for impersonation).
19
+ * @param value - The new identifier value.
20
+ */
21
+ setIdentifier(value: string): void;
22
+ getIdentifier(): string;
23
+ getHeaders(): Promise<Record<string, string>>;
24
+ }
25
+ export type RequestHook = (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export { OpenAPIParser } from "./openapi-parser.js";
2
+ export { ToolGenerator } from "./tool-generator.js";
3
+ export { ApiExecutor } from "./api-executor.js";
4
+ export * from "./auth/index.js";
@@ -0,0 +1,6 @@
1
+ // API Explorer module exports
2
+ export { OpenAPIParser } from "./openapi-parser.js";
3
+ export { ToolGenerator } from "./tool-generator.js";
4
+ export { ApiExecutor } from "./api-executor.js";
5
+ // Auth exports
6
+ export * from "./auth/index.js";
@@ -0,0 +1,31 @@
1
+ import { OpenAPI } from "openapi-types";
2
+ export interface Endpoint {
3
+ method: string;
4
+ path: string;
5
+ operationId?: string;
6
+ summary?: string;
7
+ description?: string;
8
+ tags?: string[];
9
+ parameters?: any[];
10
+ requestBody?: any;
11
+ responses?: any;
12
+ }
13
+ /**
14
+ * Service responsible for loading, parsing, and extracting data from OpenAPI specifications.
15
+ * Uses @apidevtools/swagger-parser for robust dereferencing and validation.
16
+ */
17
+ export declare class OpenAPIParser {
18
+ private spec;
19
+ private endpoints;
20
+ /**
21
+ * Loads and parses an OpenAPI specification from the given file path.
22
+ * @param specPath - Absolute path to the openapi-spec.json file.
23
+ */
24
+ loadSpec(specPath: string): Promise<void>;
25
+ getSpec(): OpenAPI.Document;
26
+ getTags(): string[];
27
+ getEndpoints(): Endpoint[];
28
+ getEndpointsByTag(tag: string): Endpoint[];
29
+ getEndpointsByTags(tags: string[]): Endpoint[];
30
+ getEndpoint(method: string, path: string): Endpoint | undefined;
31
+ }
@@ -0,0 +1,83 @@
1
+ import SwaggerParser from "@apidevtools/swagger-parser";
2
+ /**
3
+ * Service responsible for loading, parsing, and extracting data from OpenAPI specifications.
4
+ * Uses @apidevtools/swagger-parser for robust dereferencing and validation.
5
+ */
6
+ export class OpenAPIParser {
7
+ spec = null;
8
+ endpoints = [];
9
+ /**
10
+ * Loads and parses an OpenAPI specification from the given file path.
11
+ * @param specPath - Absolute path to the openapi-spec.json file.
12
+ */
13
+ async loadSpec(specPath) {
14
+ try {
15
+ this.spec = await SwaggerParser.dereference(specPath);
16
+ }
17
+ catch (error) {
18
+ throw new Error(`Failed to parse OpenAPI spec: ${error instanceof Error ? error.message : String(error)}`);
19
+ }
20
+ }
21
+ getSpec() {
22
+ if (!this.spec) {
23
+ throw new Error("Spec not loaded. Call loadSpec() first.");
24
+ }
25
+ return this.spec;
26
+ }
27
+ getTags() {
28
+ const spec = this.getSpec();
29
+ const tags = new Set();
30
+ if ('tags' in spec && spec.tags) {
31
+ spec.tags.forEach((tag) => tags.add(tag.name));
32
+ }
33
+ if ('paths' in spec && spec.paths) {
34
+ Object.entries(spec.paths).forEach(([path, pathItem]) => {
35
+ if (!pathItem)
36
+ return;
37
+ ['get', 'post', 'put', 'delete', 'patch'].forEach((method) => {
38
+ const operation = pathItem[method];
39
+ if (operation && operation.tags) {
40
+ operation.tags.forEach((tag) => tags.add(tag));
41
+ }
42
+ });
43
+ });
44
+ }
45
+ return Array.from(tags).sort();
46
+ }
47
+ getEndpoints() {
48
+ const spec = this.getSpec();
49
+ const endpoints = [];
50
+ if ('paths' in spec && spec.paths) {
51
+ Object.entries(spec.paths).forEach(([path, pathItem]) => {
52
+ if (!pathItem)
53
+ return;
54
+ ['get', 'post', 'put', 'delete', 'patch'].forEach((method) => {
55
+ const operation = pathItem[method];
56
+ if (operation) {
57
+ endpoints.push({
58
+ method: method.toUpperCase(),
59
+ path,
60
+ operationId: operation.operationId,
61
+ summary: operation.summary,
62
+ description: operation.description,
63
+ tags: operation.tags,
64
+ parameters: operation.parameters,
65
+ requestBody: operation.requestBody,
66
+ responses: operation.responses,
67
+ });
68
+ }
69
+ });
70
+ });
71
+ }
72
+ return endpoints;
73
+ }
74
+ getEndpointsByTag(tag) {
75
+ return this.getEndpoints().filter((endpoint) => endpoint.tags?.includes(tag));
76
+ }
77
+ getEndpointsByTags(tags) {
78
+ return this.getEndpoints().filter((endpoint) => endpoint.tags?.some((t) => tags.includes(t)));
79
+ }
80
+ getEndpoint(method, path) {
81
+ return this.getEndpoints().find((e) => e.method === method.toUpperCase() && e.path === path);
82
+ }
83
+ }
@@ -0,0 +1,68 @@
1
+ import { OpenAPIParser } from "./openapi-parser.js";
2
+ import { z } from "zod";
3
+ export interface ToolConfig {
4
+ name: string;
5
+ description: string;
6
+ inputSchema?: z.ZodType<any>;
7
+ }
8
+ /**
9
+ * Service that maps OpenAPI endpoints to MCP-compatible tool definitions.
10
+ * It provides the metadata for tools that an LLM can use to explore and interact with the API.
11
+ */
12
+ export declare class ToolGenerator {
13
+ private parser;
14
+ /**
15
+ * @param parser - The OpenAPIParser instance containing the parsed spec.
16
+ */
17
+ constructor(parser: OpenAPIParser);
18
+ /**
19
+ * Returns a dictionary of tool definitions with their descriptions and Zod input schemas.
20
+ * These definitions are used by MCPServer to register tools with the SDK.
21
+ */
22
+ getToolDefinitions(): {
23
+ get_tags: {
24
+ description: string;
25
+ };
26
+ get_tag_endpoints: {
27
+ description: string;
28
+ inputSchema: z.ZodObject<{
29
+ tag: z.ZodString;
30
+ }, z.core.$strip>;
31
+ };
32
+ get_tags_endpoints: {
33
+ description: string;
34
+ inputSchema: z.ZodObject<{
35
+ tags: z.ZodArray<z.ZodString>;
36
+ }, z.core.$strip>;
37
+ };
38
+ get_all_endpoints: {
39
+ description: string;
40
+ };
41
+ get_endpoint: {
42
+ description: string;
43
+ inputSchema: z.ZodObject<{
44
+ method: z.ZodString;
45
+ path: z.ZodString;
46
+ }, z.core.$strip>;
47
+ };
48
+ get_endpoints: {
49
+ description: string;
50
+ inputSchema: z.ZodObject<{
51
+ requests: z.ZodArray<z.ZodObject<{
52
+ method: z.ZodString;
53
+ path: z.ZodString;
54
+ }, z.core.$strip>>;
55
+ }, z.core.$strip>;
56
+ };
57
+ call_endpoint: {
58
+ description: string;
59
+ inputSchema: z.ZodObject<{
60
+ method: z.ZodString;
61
+ path: z.ZodString;
62
+ parameters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
63
+ body: z.ZodOptional<z.ZodAny>;
64
+ }, z.core.$strip>;
65
+ };
66
+ };
67
+ handleToolCall(name: string, args: any): any;
68
+ }
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Service that maps OpenAPI endpoints to MCP-compatible tool definitions.
4
+ * It provides the metadata for tools that an LLM can use to explore and interact with the API.
5
+ */
6
+ export class ToolGenerator {
7
+ parser;
8
+ /**
9
+ * @param parser - The OpenAPIParser instance containing the parsed spec.
10
+ */
11
+ constructor(parser) {
12
+ this.parser = parser;
13
+ }
14
+ /**
15
+ * Returns a dictionary of tool definitions with their descriptions and Zod input schemas.
16
+ * These definitions are used by MCPServer to register tools with the SDK.
17
+ */
18
+ getToolDefinitions() {
19
+ return {
20
+ get_tags: {
21
+ description: "Get all unique tags defined in the API spec. This helps to group and discover endpoints.",
22
+ },
23
+ get_tag_endpoints: {
24
+ description: "Get all endpoints associated with a specific tag. Returns a summary of each endpoint.",
25
+ inputSchema: z.object({
26
+ tag: z.string().describe("The tag to filter endpoints by."),
27
+ }),
28
+ },
29
+ get_tags_endpoints: {
30
+ description: "Get all endpoints associated with multiple tags. Returns a summary of each endpoint.",
31
+ inputSchema: z.object({
32
+ tags: z.array(z.string()).describe("The tags to filter endpoints by."),
33
+ }),
34
+ },
35
+ get_all_endpoints: {
36
+ description: "Get a summarized list of all endpoints available in the API.",
37
+ },
38
+ get_endpoint: {
39
+ description: "Get detailed information about a specific endpoint, including parameters and request body schema.",
40
+ inputSchema: z.object({
41
+ method: z.string().describe("The HTTP method (GET, POST, etc.)."),
42
+ path: z.string().describe("The endpoint path."),
43
+ }),
44
+ },
45
+ get_endpoints: {
46
+ description: "Get detailed information for multiple specific endpoints.",
47
+ inputSchema: z.object({
48
+ requests: z.array(z.object({
49
+ method: z.string(),
50
+ path: z.string(),
51
+ })).describe("List of endpoint requests."),
52
+ }),
53
+ },
54
+ call_endpoint: {
55
+ description: "Execute a request to a project's endpoint using the specified parameters and body.",
56
+ inputSchema: z.object({
57
+ method: z.string().describe("The HTTP method."),
58
+ path: z.string().describe("The endpoint path (e.g., /projects/{id})."),
59
+ parameters: z.record(z.string(), z.any()).optional().describe("Path and query parameters (mapped by name)."),
60
+ body: z.any().optional().describe("The request body payload."),
61
+ }),
62
+ },
63
+ };
64
+ }
65
+ handleToolCall(name, args) {
66
+ switch (name) {
67
+ case "get_tags":
68
+ return this.parser.getTags();
69
+ case "get_tag_endpoints":
70
+ return this.parser.getEndpointsByTag(args.tag);
71
+ case "get_tags_endpoints":
72
+ return this.parser.getEndpointsByTags(args.tags);
73
+ case "get_all_endpoints":
74
+ return this.parser.getEndpoints();
75
+ case "get_endpoint":
76
+ return this.parser.getEndpoint(args.method, args.path);
77
+ case "get_endpoints":
78
+ return args.requests.map((r) => this.parser.getEndpoint(r.method, r.path));
79
+ default:
80
+ throw new Error(`Unknown tool: ${name}`);
81
+ }
82
+ }
83
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { MCPServer } from "./index.js";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const DEFAULT_SPEC_PATH = path.resolve(__dirname, "../openapi-spec.json");
7
+ const specPath = process.argv[2] || DEFAULT_SPEC_PATH;
8
+ const server = new MCPServer(specPath);
9
+ server.start().catch((error) => {
10
+ console.error("Fatal error in MCP server:", error);
11
+ process.exit(1);
12
+ });
@@ -0,0 +1 @@
1
+ export { MCPServer } from "./mcp-server.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { MCPServer } from "./mcp-server.js";
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The main MCP Server implementation that coordinates the OpenAPI parser,
3
+ * tool generation, and API execution.
4
+ *
5
+ * It registers meta-tools that allow LLMs to:
6
+ * 1. Discover API structure (tags, endpoints, schemas).
7
+ * 2. Execute calls to any endpoint with dynamic authentication.
8
+ * 3. Impersonate users via the set_identity tool.
9
+ */
10
+ export declare class MCPServer {
11
+ private specPath;
12
+ private server;
13
+ private parser;
14
+ private toolGenerator;
15
+ private apiExecutor;
16
+ private authContext;
17
+ constructor(specPath: string);
18
+ private initParser;
19
+ private registerTools;
20
+ /**
21
+ * Starts the MCP server on Stdio transport.
22
+ * This method ensures the OpenAPI spec is loaded and tools are registered before connecting.
23
+ */
24
+ start(): Promise<void>;
25
+ }
@@ -0,0 +1,103 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { OpenAPIParser } from "./api-explorer/openapi-parser.js";
5
+ import { ToolGenerator } from "./api-explorer/tool-generator.js";
6
+ import { ApiExecutor } from "./api-explorer/api-executor.js";
7
+ import { createAuthContextFromEnv } from "./api-explorer/auth/index.js";
8
+ /**
9
+ * The main MCP Server implementation that coordinates the OpenAPI parser,
10
+ * tool generation, and API execution.
11
+ *
12
+ * It registers meta-tools that allow LLMs to:
13
+ * 1. Discover API structure (tags, endpoints, schemas).
14
+ * 2. Execute calls to any endpoint with dynamic authentication.
15
+ * 3. Impersonate users via the set_identity tool.
16
+ */
17
+ export class MCPServer {
18
+ specPath;
19
+ server;
20
+ parser;
21
+ toolGenerator;
22
+ apiExecutor;
23
+ authContext;
24
+ constructor(specPath) {
25
+ this.specPath = specPath;
26
+ this.parser = new OpenAPIParser();
27
+ this.toolGenerator = new ToolGenerator(this.parser);
28
+ this.authContext = createAuthContextFromEnv();
29
+ this.apiExecutor = new ApiExecutor(undefined, this.authContext);
30
+ this.server = new McpServer({
31
+ name: "project-mcp-server",
32
+ version: "1.0.0",
33
+ });
34
+ }
35
+ async initParser() {
36
+ try {
37
+ await this.parser.loadSpec(this.specPath);
38
+ console.error(`Loaded OpenAPI spec from ${this.specPath}`);
39
+ this.registerTools();
40
+ }
41
+ catch (error) {
42
+ console.error(`Failed to load OpenAPI spec: ${error}`);
43
+ process.exit(1);
44
+ }
45
+ }
46
+ registerTools() {
47
+ const definitions = this.toolGenerator.getToolDefinitions();
48
+ Object.entries(definitions).forEach(([name, def]) => {
49
+ this.server.registerTool(name, {
50
+ description: def.description,
51
+ inputSchema: def.inputSchema,
52
+ }, async (args) => {
53
+ try {
54
+ let result;
55
+ if (name === "call_endpoint") {
56
+ result = await this.apiExecutor.callEndpoint(args);
57
+ }
58
+ else {
59
+ result = this.toolGenerator.handleToolCall(name, args);
60
+ }
61
+ return {
62
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
63
+ };
64
+ }
65
+ catch (error) {
66
+ return {
67
+ content: [{ type: "text", text: `Error: ${error.message}` }],
68
+ isError: true,
69
+ };
70
+ }
71
+ });
72
+ });
73
+ this.server.registerTool("set_identity", {
74
+ description: "Set the identity (identifier) for future API requests. Requires an 'identity' auth strategy to be active.",
75
+ inputSchema: z.object({
76
+ identifier: z.string().describe("The new identity value (e.g., user UID)."),
77
+ }),
78
+ }, async ({ identifier }) => {
79
+ try {
80
+ this.authContext.setIdentifier(identifier);
81
+ return {
82
+ content: [{ type: "text", text: `Successfully updated identity to: ${identifier}` }],
83
+ };
84
+ }
85
+ catch (error) {
86
+ return {
87
+ content: [{ type: "text", text: `Error: ${error.message}` }],
88
+ isError: true,
89
+ };
90
+ }
91
+ });
92
+ }
93
+ /**
94
+ * Starts the MCP server on Stdio transport.
95
+ * This method ensures the OpenAPI spec is loaded and tools are registered before connecting.
96
+ */
97
+ async start() {
98
+ await this.initParser();
99
+ const transport = new StdioServerTransport();
100
+ await this.server.connect(transport);
101
+ console.error("OpenAPI MCP server running on stdio");
102
+ }
103
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@williamp29/project-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "A ModelContextProtocol server to let agents discover your project, such as APIs (using OpenAPI) or other resources.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "project-mcp-server": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ },
20
+ "./api-explorer": {
21
+ "types": "./dist/api-explorer/index.d.ts",
22
+ "import": "./dist/api-explorer/index.js"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "start": "node dist/cli.js",
28
+ "dev": "ts-node src/cli.ts",
29
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
30
+ },
31
+ "dependencies": {
32
+ "@apidevtools/swagger-parser": "^12.1.0",
33
+ "@modelcontextprotocol/inspector": "^0.18.0",
34
+ "@modelcontextprotocol/sdk": "^1.25.2",
35
+ "axios": "^1.13.2",
36
+ "dotenv": "^17.2.3",
37
+ "g": "^2.0.1",
38
+ "openapi-types": "^12.1.3",
39
+ "zod": "^4.3.5"
40
+ },
41
+ "devDependencies": {
42
+ "@types/jest": "^30.0.0",
43
+ "@types/node": "^25.0.9",
44
+ "@types/node-fetch": "^2.6.13",
45
+ "@types/swagger-parser": "^4.0.3",
46
+ "jest": "^30.2.0",
47
+ "ts-jest": "^29.4.6",
48
+ "ts-node": "^10.9.2",
49
+ "typescript": "^5.9.3"
50
+ }
51
+ }