teachable-mcp-server
Version:
MCP Server for Teachable API integration - manage courses, users, enrollments and more
709 lines • 44.6 kB
JavaScript
/**
* MCP Server generated from OpenAPI spec for teachable-public-api v0.0.1
* Generated on: 2025-08-31T05:18:42.766Z
*/
// Load environment variables from .env file
import dotenv from 'dotenv';
dotenv.config();
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { z, ZodError } from 'zod';
import { jsonSchemaToZod } from 'json-schema-to-zod';
import axios from 'axios';
/**
* Server configuration
*/
export const SERVER_NAME = "teachable-public-api";
export const SERVER_VERSION = "0.0.1";
export const API_BASE_URL = "https://developers.teachable.com";
/**
* MCP Server instance
*/
const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
/**
* Map of tool definitions by name
*/
const toolDefinitionMap = new Map([
["ListCourses", {
name: "ListCourses",
description: `Fetch all courses at your school.`,
inputSchema: { "type": "object", "properties": { "name": { "type": "string", "description": "Filter courses by course name" }, "is_published": { "type": "boolean", "description": "Filter courses by published status. If true, return published courses. If false, return unpublished courses." }, "author_bio_id": { "type": "number", "format": "int32", "description": "Filter courses by a specific course author via the course author's bio ID." }, "created_at": { "type": "string", "format": "date-time", "description": "Return courses by the date & time of course creation. Formatted in ISO8601." }, "page": { "type": "number", "format": "int32", "description": "Used in pagination when number of courses exceed the maximum amount of results per page" }, "per": { "type": "number", "format": "int32", "description": "Used in pagination to define amount of courses per page, when not defined the maximum is 20" } } },
method: "get",
pathTemplate: "/v1/courses",
executionParameters: [{ "name": "name", "in": "query" }, { "name": "is_published", "in": "query" }, { "name": "author_bio_id", "in": "query" }, { "name": "created_at", "in": "query" }, { "name": "page", "in": "query" }, { "name": "per", "in": "query" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ShowCourse", {
name: "ShowCourse",
description: `Fetch a specific course by ID.`,
inputSchema: { "type": "object", "properties": { "course_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return a course by its unique ID." } }, "required": ["course_id"] },
method: "get",
pathTemplate: "/v1/courses/{course_id}",
executionParameters: [{ "name": "course_id", "in": "path" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ShowCourseEnrollments", {
name: "ShowCourseEnrollments",
description: `Fetch active enrolled students and student progress for a specific course.`,
inputSchema: { "type": "object", "properties": { "course_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return enrollments for a specific course by the unique course ID." }, "enrolled_in_after": { "type": "string", "format": "date-time", "description": "Search for students who are enrolled after a specific date/time. Formatted in ISO8601." }, "enrolled_in_before": { "type": "string", "format": "date-time", "description": "Search for students who are enrolled before a specific date/time. Formatted in ISO8601." }, "sort_direction": { "type": "string", "enum": ["asc", "desc"], "description": "Enrollments are sorted by the 'enrolled_at' datetime. You can choose the direction by including the sort_direction param." } }, "required": ["course_id"] },
method: "get",
pathTemplate: "/v1/courses/{course_id}/enrollments",
executionParameters: [{ "name": "course_id", "in": "path" }, { "name": "enrolled_in_after", "in": "query" }, { "name": "enrolled_in_before", "in": "query" }, { "name": "sort_direction", "in": "query" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ShowLecture", {
name: "ShowLecture",
description: `Fetch content of a specific course lecture.`,
inputSchema: { "type": "object", "properties": { "course_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique course ID that contains the lecture." }, "lecture_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique lecture ID." } }, "required": ["course_id", "lecture_id"] },
method: "get",
pathTemplate: "/v1/courses/{course_id}/lectures/{lecture_id}",
executionParameters: [{ "name": "course_id", "in": "path" }, { "name": "lecture_id", "in": "path" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["MarkLectureComplete", {
name: "MarkLectureComplete",
description: `Mark a specific course lecture as complete.`,
inputSchema: { "type": "object", "properties": { "course_id": { "type": "number", "format": "int32", "minimum": 1, "description": "The unique course ID that contains the lecture." }, "lecture_id": { "type": "number", "format": "int32", "minimum": 1, "description": "The unique lecture ID." }, "requestBody": { "type": "object", "properties": { "user_id": { "type": "number", "format": "int32", "description": "The unique ID of the user." } }, "required": ["user_id"], "description": "The JSON request body." } }, "required": ["course_id", "lecture_id", "requestBody"] },
method: "post",
pathTemplate: "/v1/courses/{course_id}/lectures/{lecture_id}/mark_complete",
executionParameters: [{ "name": "course_id", "in": "path" }, { "name": "lecture_id", "in": "path" }],
requestBodyContentType: "application/json",
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["CourseProgress", {
name: "CourseProgress",
description: `Fetch a specific user's course progress.`,
inputSchema: { "type": "object", "properties": { "course_id": { "type": "number", "format": "int32", "minimum": 1, "description": "The unique course ID that contains the lecture." }, "user_id": { "type": "number", "format": "int32", "description": "The unique ID of the user." }, "page": { "type": "number", "format": "int32", "description": "Used in pagination when number of courses exceed the maximum amount of results per page" }, "per": { "type": "number", "format": "int32", "description": "Used in pagination to define amount of courses per page, when not defined the maximum is 20" } }, "required": ["course_id", "user_id"] },
method: "get",
pathTemplate: "/v1/courses/{course_id}/progress",
executionParameters: [{ "name": "course_id", "in": "path" }, { "name": "user_id", "in": "query" }, { "name": "page", "in": "query" }, { "name": "per", "in": "query" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ListQuizzes", {
name: "ListQuizzes",
description: `Fetch an id list of quizzes in a specific course lecture.`,
inputSchema: { "type": "object", "properties": { "course_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique course ID that contains the lecture." }, "lecture_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique lecture ID." } }, "required": ["course_id", "lecture_id"] },
method: "get",
pathTemplate: "/v1/courses/{course_id}/lectures/{lecture_id}/quizzes",
executionParameters: [{ "name": "course_id", "in": "path" }, { "name": "lecture_id", "in": "path" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ShowQuiz", {
name: "ShowQuiz",
description: `Fetch a specific quiz information.`,
inputSchema: { "type": "object", "properties": { "course_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique course ID that contains the lecture." }, "lecture_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique lecture ID." }, "quiz_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique quiz attachment ID." } }, "required": ["course_id", "lecture_id", "quiz_id"] },
method: "get",
pathTemplate: "/v1/courses/{course_id}/lectures/{lecture_id}/quizzes/{quiz_id}",
executionParameters: [{ "name": "course_id", "in": "path" }, { "name": "lecture_id", "in": "path" }, { "name": "quiz_id", "in": "path" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ShowQuizResponses", {
name: "ShowQuizResponses",
description: `Fetch the responses of quiz.`,
inputSchema: { "type": "object", "properties": { "course_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique course ID that contains the lecture." }, "lecture_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique lecture ID." }, "quiz_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique quiz attachment ID." } }, "required": ["course_id", "lecture_id", "quiz_id"] },
method: "get",
pathTemplate: "/v1/courses/{course_id}/lectures/{lecture_id}/quizzes/{quiz_id}/responses",
executionParameters: [{ "name": "course_id", "in": "path" }, { "name": "lecture_id", "in": "path" }, { "name": "quiz_id", "in": "path" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ShowVideo", {
name: "ShowVideo",
description: `Fetch a specific video information.`,
inputSchema: { "type": "object", "properties": { "course_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique course ID that contains the lecture." }, "lecture_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique lecture ID." }, "video_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Return results by unique video attachment ID." }, "user_id": { "type": "number", "format": "int32", "description": "Specify the user who is watching the video" } }, "required": ["course_id", "lecture_id", "video_id"] },
method: "get",
pathTemplate: "/v1/courses/{course_id}/lectures/{lecture_id}/videos/{video_id}",
executionParameters: [{ "name": "course_id", "in": "path" }, { "name": "lecture_id", "in": "path" }, { "name": "video_id", "in": "path" }, { "name": "user_id", "in": "query" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ListUsers", {
name: "ListUsers",
description: `Get a list of users`,
inputSchema: { "type": "object", "properties": { "page": { "type": "number", "format": "int32", "description": "Used in pagination when number of users exceed the maximum amount of results per page" }, "per": { "type": "number", "format": "int32", "description": "Used in pagination to define amount of users per page, when not defined the maximum is 20" }, "search_after": { "type": "number", "format": "int32", "description": "Used when number of users exceeds 10,000 records. Use the search_after value in the parameters to search the next set of records." }, "email": { "type": "string", "description": "Filter users by user email." } } },
method: "get",
pathTemplate: "/v1/users",
executionParameters: [{ "name": "page", "in": "query" }, { "name": "per", "in": "query" }, { "name": "search_after", "in": "query" }, { "name": "email", "in": "query" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["CreateUser", {
name: "CreateUser",
description: `Create a new user`,
inputSchema: { "type": "object", "properties": { "requestBody": { "type": "object", "properties": { "name": { "type": "string", "description": "The name of the new user." }, "email": { "type": "string", "description": "The email address of the new user.." }, "password": { "type": "string", "description": "The password of the new user. Must be at least 6 characters. If no password is set, the student will be sent an email to set a password and confirm their account." }, "src": { "type": "string", "description": "The [signup source](https://support.teachable.com/hc/en-us/articles/219571648#TrackSignupSourceshttps://support.teachable.com/hc/en-us/articles/219571648#TrackSignupSources) of the user, Information tab of the user profile.\nSRC can also be used as a custom value when creating users in your school.\nFor example, if you use any unique identifiers to help manage your users in multiple external systems (such as unique IDs, tags, etc.),\nyou can use the src field to keep this identifier associated with your user in Teachable." } }, "required": ["email"], "description": "The JSON request body." } }, "required": ["requestBody"] },
method: "post",
pathTemplate: "/v1/users",
executionParameters: [],
requestBodyContentType: "application/json",
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ShowUser", {
name: "ShowUser",
description: `List a specific user and their course enrollments by user ID.`,
inputSchema: { "type": "object", "properties": { "user_id": { "type": "number", "format": "int32", "minimum": 1, "description": "The unique ID of the user." } }, "required": ["user_id"] },
method: "get",
pathTemplate: "/v1/users/{user_id}",
executionParameters: [{ "name": "user_id", "in": "path" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["UpdateUser", {
name: "UpdateUser",
description: `Update the name or src of a user.`,
inputSchema: { "type": "object", "properties": { "user_id": { "type": "number", "format": "int32", "minimum": 1, "description": "The unique ID of the user." }, "requestBody": { "type": "object", "properties": { "name": { "type": "string", "description": "The name of the user." }, "src": { "type": "string", "description": "The signup source of the user, which is displayed on the Information tab of the user profile. ." } }, "description": "The JSON request body." } }, "required": ["user_id", "requestBody"] },
method: "patch",
pathTemplate: "/v1/users/{user_id}",
executionParameters: [{ "name": "user_id", "in": "path" }],
requestBodyContentType: "application/json",
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["EnrollUser", {
name: "EnrollUser",
description: `Enroll a user in a course.`,
inputSchema: { "type": "object", "properties": { "requestBody": { "type": "object", "properties": { "user_id": { "type": "number", "format": "int32", "description": "The unique ID of the user." }, "course_id": { "type": "number", "format": "int32", "description": "The unique ID of the course." } }, "required": ["user_id", "course_id"], "description": "The JSON request body." } }, "required": ["requestBody"] },
method: "post",
pathTemplate: "/v1/enroll",
executionParameters: [],
requestBodyContentType: "application/json",
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["UnenrollUser", {
name: "UnenrollUser",
description: `Unenroll a user from a course.`,
inputSchema: { "type": "object", "properties": { "requestBody": { "type": "object", "properties": { "user_id": { "type": "number", "format": "int32", "description": "The unique ID of the user." }, "course_id": { "type": "number", "format": "int32", "description": "The unique ID of the course." } }, "required": ["user_id", "course_id"], "description": "The JSON request body." } }, "required": ["requestBody"] },
method: "post",
pathTemplate: "/v1/unenroll",
executionParameters: [],
requestBodyContentType: "application/json",
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ListWebhooks", {
name: "ListWebhooks",
description: `Fetch all webhook events for your school.`,
inputSchema: { "type": "object", "properties": {} },
method: "get",
pathTemplate: "/v1/webhooks",
executionParameters: [],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ShowWebhookEvents", {
name: "ShowWebhookEvents",
description: `Fetch all the events for a webhook.`,
inputSchema: { "type": "object", "properties": { "webhook_id": { "type": "number", "format": "int32", "description": "The unique ID of the webhook." }, "response_http_status_gte": { "type": "number", "format": "int32", "description": "Filter responses by HTTP status code of the webhook event, greater than or equal to the provided value (i.e., enter 200 to search for webhook events that had an HTTP status code of 200 or greater)." }, "response_http_status_lte": { "type": "number", "format": "int32", "description": "Filter responses by HTTP status of the webhook event, less than or equal to the provided value. (e.g., enter 200 to search for webhook events that had an HTTP status code of 200 or less)." }, "created_before": { "type": "string", "format": "date-time", "default": "2020-04-17T19:44:03Z", "description": "Search for webhook events that were created before a specific date/time. Formatted in ISO 8601." }, "created_after": { "type": "string", "format": "date-time", "default": "2020-04-17T19:44:03Z", "description": "Search for webhook events that were created after a specific date/time. Formatted in ISO 8601." }, "page": { "type": "number", "format": "int32", "description": "Set the page number to be returned. (i.e., If you have two pages of results with 20 results per page, set the page value to 1 to receive results 1 through 20, or set the page value to 2 to receive results 21-40)." }, "per": { "type": "number", "format": "int32", "description": "Set the maximum number of results to be returned by page. By default, each page will return 20 results." } }, "required": ["webhook_id"] },
method: "get",
pathTemplate: "/v1/webhooks/{webhook_id}/events",
executionParameters: [{ "name": "webhook_id", "in": "path" }, { "name": "response_http_status_gte", "in": "query" }, { "name": "response_http_status_lte", "in": "query" }, { "name": "created_before", "in": "query" }, { "name": "created_after", "in": "query" }, { "name": "page", "in": "query" }, { "name": "per", "in": "query" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ShowPricingPlans", {
name: "ShowPricingPlans",
description: `Fetch details of a specific pricing plan. Currently only supports pricing plans associated with courses.`,
inputSchema: { "type": "object", "properties": { "pricing_plan_id": { "type": "number", "format": "int32", "minimum": 1, "description": "Search for a pricing plan by its unique ID." } }, "required": ["pricing_plan_id"] },
method: "get",
pathTemplate: "/v1/pricing_plans/{pricing_plan_id}",
executionParameters: [{ "name": "pricing_plan_id", "in": "path" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ListPricingPlans", {
name: "ListPricingPlans",
description: `Fetch all the pricing plans at your school`,
inputSchema: { "type": "object", "properties": { "page": { "type": "number", "format": "int32", "description": "Used in pagination when number of pricing plans exceeds the maximum amount of results per page" }, "per": { "type": "number", "format": "int32", "description": "Used in pagination to define amount of pricing plans per page, when not defined the maximum is 5" } } },
method: "get",
pathTemplate: "/v1/pricing_plans",
executionParameters: [{ "name": "page", "in": "query" }, { "name": "per", "in": "query" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
["ListTransactions", {
name: "ListTransactions",
description: `Fetch a list of sales transactions made in your school. (New transactions can take up to two minutes to be returned via API call from the time of sale.)`,
inputSchema: { "type": "object", "properties": { "user_id": { "type": "number", "format": "int32" }, "affiliate_id": { "type": "number", "format": "int32" }, "course_id": { "type": "number", "format": "int32" }, "pricing_plan_id": { "type": "number", "format": "int32" }, "is_fully_refunded": { "type": "boolean" }, "is_chargeback": { "type": "boolean" }, "start": { "type": "string", "format": "date-time", "description": "The beginning of the time period to return results for (exclusive), in ISO8601 format." }, "end": { "type": "string", "format": "date-time", "description": "The end of the time period to return results for (inclusive), in ISO8601 format." }, "page": { "type": "number", "format": "int32", "description": "Used in pagination when number of transactions exceed the maximum amount of results per page" }, "per": { "type": "number", "format": "int32", "description": "Used in pagination to define amount of transactions per page, when not defined the maximum is 20" } } },
method: "get",
pathTemplate: "/v1/transactions",
executionParameters: [{ "name": "user_id", "in": "query" }, { "name": "affiliate_id", "in": "query" }, { "name": "course_id", "in": "query" }, { "name": "pricing_plan_id", "in": "query" }, { "name": "is_fully_refunded", "in": "query" }, { "name": "is_chargeback", "in": "query" }, { "name": "start", "in": "query" }, { "name": "end", "in": "query" }, { "name": "page", "in": "query" }, { "name": "per", "in": "query" }],
requestBodyContentType: undefined,
securityRequirements: [{ "ApiKeyAuth": [] }]
}],
]);
/**
* Security schemes from the OpenAPI spec
*/
const securitySchemes = {
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "apiKey"
}
};
server.setRequestHandler(ListToolsRequestSchema, async () => {
const toolsForClient = Array.from(toolDefinitionMap.values()).map(def => ({
name: def.name,
description: def.description,
inputSchema: def.inputSchema
}));
return { tools: toolsForClient };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name: toolName, arguments: toolArgs } = request.params;
const toolDefinition = toolDefinitionMap.get(toolName);
if (!toolDefinition) {
console.error(`Error: Unknown tool requested: ${toolName}`);
return { content: [{ type: "text", text: `Error: Unknown tool requested: ${toolName}` }] };
}
return await executeApiTool(toolName, toolDefinition, toolArgs ?? {}, securitySchemes);
});
/**
* Acquires an OAuth2 token using client credentials flow
*
* @param schemeName Name of the security scheme
* @param scheme OAuth2 security scheme
* @returns Acquired token or null if unable to acquire
*/
async function acquireOAuth2Token(schemeName, scheme) {
try {
// Check if we have the necessary credentials
const clientId = process.env[`OAUTH_CLIENT_ID_SCHEMENAME`];
const clientSecret = process.env[`OAUTH_CLIENT_SECRET_SCHEMENAME`];
const scopes = process.env[`OAUTH_SCOPES_SCHEMENAME`];
if (!clientId || !clientSecret) {
console.error(`Missing client credentials for OAuth2 scheme '${schemeName}'`);
return null;
}
// Initialize token cache if needed
if (typeof global.__oauthTokenCache === 'undefined') {
global.__oauthTokenCache = {};
}
// Check if we have a cached token
const cacheKey = `${schemeName}_${clientId}`;
const cachedToken = global.__oauthTokenCache[cacheKey];
const now = Date.now();
if (cachedToken && cachedToken.expiresAt > now) {
console.error(`Using cached OAuth2 token for '${schemeName}' (expires in ${Math.floor((cachedToken.expiresAt - now) / 1000)} seconds)`);
return cachedToken.token;
}
// Determine token URL based on flow type
let tokenUrl = '';
if (scheme.flows?.clientCredentials?.tokenUrl) {
tokenUrl = scheme.flows.clientCredentials.tokenUrl;
console.error(`Using client credentials flow for '${schemeName}'`);
}
else if (scheme.flows?.password?.tokenUrl) {
tokenUrl = scheme.flows.password.tokenUrl;
console.error(`Using password flow for '${schemeName}'`);
}
else {
console.error(`No supported OAuth2 flow found for '${schemeName}'`);
return null;
}
// Prepare the token request
let formData = new URLSearchParams();
formData.append('grant_type', 'client_credentials');
// Add scopes if specified
if (scopes) {
formData.append('scope', scopes);
}
console.error(`Requesting OAuth2 token from ${tokenUrl}`);
// Make the token request
const response = await axios({
method: 'POST',
url: tokenUrl,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`
},
data: formData.toString()
});
// Process the response
if (response.data?.access_token) {
const token = response.data.access_token;
const expiresIn = response.data.expires_in || 3600; // Default to 1 hour
// Cache the token
global.__oauthTokenCache[cacheKey] = {
token,
expiresAt: now + (expiresIn * 1000) - 60000 // Expire 1 minute early
};
console.error(`Successfully acquired OAuth2 token for '${schemeName}' (expires in ${expiresIn} seconds)`);
return token;
}
else {
console.error(`Failed to acquire OAuth2 token for '${schemeName}': No access_token in response`);
return null;
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Error acquiring OAuth2 token for '${schemeName}':`, errorMessage);
return null;
}
}
/**
* Executes an API tool with the provided arguments
*
* @param toolName Name of the tool to execute
* @param definition Tool definition
* @param toolArgs Arguments provided by the user
* @param allSecuritySchemes Security schemes from the OpenAPI spec
* @returns Call tool result
*/
async function executeApiTool(toolName, definition, toolArgs, allSecuritySchemes) {
try {
// Validate arguments against the input schema
let validatedArgs;
try {
const zodSchema = getZodSchemaFromJsonSchema(definition.inputSchema, toolName);
const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {};
validatedArgs = zodSchema.parse(argsToParse);
}
catch (error) {
if (error instanceof ZodError) {
const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map(e => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`;
return { content: [{ type: 'text', text: validationErrorMessage }] };
}
else {
const errorMessage = error instanceof Error ? error.message : String(error);
return { content: [{ type: 'text', text: `Internal error during validation setup: ${errorMessage}` }] };
}
}
// Prepare URL, query parameters, headers, and request body
let urlPath = definition.pathTemplate;
const queryParams = {};
const headers = { 'Accept': 'application/json' };
let requestBodyData = undefined;
// Apply parameters to the URL path, query, or headers
definition.executionParameters.forEach((param) => {
const value = validatedArgs[param.name];
if (typeof value !== 'undefined' && value !== null) {
if (param.in === 'path') {
urlPath = urlPath.replace(`{${param.name}}`, encodeURIComponent(String(value)));
}
else if (param.in === 'query') {
queryParams[param.name] = value;
}
else if (param.in === 'header') {
headers[param.name.toLowerCase()] = String(value);
}
}
});
// Ensure all path parameters are resolved
if (urlPath.includes('{')) {
throw new Error(`Failed to resolve path parameters: ${urlPath}`);
}
// Construct the full URL
const requestUrl = API_BASE_URL ? `${API_BASE_URL}${urlPath}` : urlPath;
// Handle request body if needed
if (definition.requestBodyContentType && typeof validatedArgs['requestBody'] !== 'undefined') {
requestBodyData = validatedArgs['requestBody'];
headers['content-type'] = definition.requestBodyContentType;
}
// Apply security requirements if available
// Security requirements use OR between array items and AND within each object
const appliedSecurity = definition.securityRequirements?.find(req => {
// Try each security requirement (combined with OR)
return Object.entries(req).every(([schemeName, scopesArray]) => {
const scheme = allSecuritySchemes[schemeName];
if (!scheme)
return false;
// API Key security (header, query, cookie)
if (scheme.type === 'apiKey') {
return !!process.env[`API_KEY_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`];
}
// HTTP security (basic, bearer)
if (scheme.type === 'http') {
if (scheme.scheme?.toLowerCase() === 'bearer') {
return !!process.env[`BEARER_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`];
}
else if (scheme.scheme?.toLowerCase() === 'basic') {
return !!process.env[`BASIC_USERNAME_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`] &&
!!process.env[`BASIC_PASSWORD_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`];
}
}
// OAuth2 security
if (scheme.type === 'oauth2') {
// Check for pre-existing token
if (process.env[`OAUTH_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]) {
return true;
}
// Check for client credentials for auto-acquisition
if (process.env[`OAUTH_CLIENT_ID_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`] &&
process.env[`OAUTH_CLIENT_SECRET_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`]) {
// Verify we have a supported flow
if (scheme.flows?.clientCredentials || scheme.flows?.password) {
return true;
}
}
return false;
}
// OpenID Connect
if (scheme.type === 'openIdConnect') {
return !!process.env[`OPENID_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`];
}
return false;
});
});
// If we found matching security scheme(s), apply them
if (appliedSecurity) {
// Apply each security scheme from this requirement (combined with AND)
for (const [schemeName, scopesArray] of Object.entries(appliedSecurity)) {
const scheme = allSecuritySchemes[schemeName];
// API Key security
if (scheme?.type === 'apiKey') {
const apiKey = process.env[`API_KEY_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`];
if (apiKey) {
if (scheme.in === 'header') {
headers[scheme.name.toLowerCase()] = apiKey;
console.error(`Applied API key '${schemeName}' in header '${scheme.name}'`);
}
else if (scheme.in === 'query') {
queryParams[scheme.name] = apiKey;
console.error(`Applied API key '${schemeName}' in query parameter '${scheme.name}'`);
}
else if (scheme.in === 'cookie') {
// Add the cookie, preserving other cookies if they exist
headers['cookie'] = `${scheme.name}=${apiKey}${headers['cookie'] ? `; ${headers['cookie']}` : ''}`;
console.error(`Applied API key '${schemeName}' in cookie '${scheme.name}'`);
}
}
}
// HTTP security (Bearer or Basic)
else if (scheme?.type === 'http') {
if (scheme.scheme?.toLowerCase() === 'bearer') {
const token = process.env[`BEARER_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`];
if (token) {
headers['authorization'] = `Bearer ${token}`;
console.error(`Applied Bearer token for '${schemeName}'`);
}
}
else if (scheme.scheme?.toLowerCase() === 'basic') {
const username = process.env[`BASIC_USERNAME_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`];
const password = process.env[`BASIC_PASSWORD_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`];
if (username && password) {
headers['authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
console.error(`Applied Basic authentication for '${schemeName}'`);
}
}
}
// OAuth2 security
else if (scheme?.type === 'oauth2') {
// First try to use a pre-provided token
let token = process.env[`OAUTH_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`];
// If no token but we have client credentials, try to acquire a token
if (!token && (scheme.flows?.clientCredentials || scheme.flows?.password)) {
console.error(`Attempting to acquire OAuth token for '${schemeName}'`);
token = (await acquireOAuth2Token(schemeName, scheme)) ?? '';
}
// Apply token if available
if (token) {
headers['authorization'] = `Bearer ${token}`;
console.error(`Applied OAuth2 token for '${schemeName}'`);
// List the scopes that were requested, if any
const scopes = scopesArray;
if (scopes && scopes.length > 0) {
console.error(`Requested scopes: ${scopes.join(', ')}`);
}
}
}
// OpenID Connect
else if (scheme?.type === 'openIdConnect') {
const token = process.env[`OPENID_TOKEN_${schemeName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`];
if (token) {
headers['authorization'] = `Bearer ${token}`;
console.error(`Applied OpenID Connect token for '${schemeName}'`);
// List the scopes that were requested, if any
const scopes = scopesArray;
if (scopes && scopes.length > 0) {
console.error(`Requested scopes: ${scopes.join(', ')}`);
}
}
}
}
}
// Log warning if security is required but not available
else if (definition.securityRequirements?.length > 0) {
// First generate a more readable representation of the security requirements
const securityRequirementsString = definition.securityRequirements
.map(req => {
const parts = Object.entries(req)
.map(([name, scopesArray]) => {
const scopes = scopesArray;
if (scopes.length === 0)
return name;
return `${name} (scopes: ${scopes.join(', ')})`;
})
.join(' AND ');
return `[${parts}]`;
})
.join(' OR ');
console.warn(`Tool '${toolName}' requires security: ${securityRequirementsString}, but no suitable credentials found.`);
}
// Prepare the axios request configuration
const config = {
method: definition.method.toUpperCase(),
url: requestUrl,
params: queryParams,
headers: headers,
...(requestBodyData !== undefined && { data: requestBodyData }),
};
// Log request info to stderr (doesn't affect MCP output)
console.error(`Executing tool "${toolName}": ${config.method} ${config.url}`);
// Execute the request
const response = await axios(config);
// Process and format the response
let responseText = '';
const contentType = response.headers['content-type']?.toLowerCase() || '';
// Handle JSON responses
if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) {
try {
responseText = JSON.stringify(response.data, null, 2);
}
catch (e) {
responseText = "[Stringify Error]";
}
}
// Handle string responses
else if (typeof response.data === 'string') {
responseText = response.data;
}
// Handle other response types
else if (response.data !== undefined && response.data !== null) {
responseText = String(response.data);
}
// Handle empty responses
else {
responseText = `(Status: ${response.status} - No body content)`;
}
// Return formatted response
return {
content: [
{
type: "text",
text: `API Response (Status: ${response.status}):\n${responseText}`
}
],
};
}
catch (error) {
// Handle errors during execution
let errorMessage;
// Format Axios errors specially
if (axios.isAxiosError(error)) {
errorMessage = formatApiError(error);
}
// Handle standard errors
else if (error instanceof Error) {
errorMessage = error.message;
}
// Handle unexpected error types
else {
errorMessage = 'Unexpected error: ' + String(error);
}
// Log error to stderr
console.error(`Error during execution of tool '${toolName}':`, errorMessage);
// Return error message to client
return { content: [{ type: "text", text: errorMessage }] };
}
}
/**
* Main function to start the server
*/
async function main() {
// Set up stdio transport
try {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${SERVER_NAME} MCP Server (v${SERVER_VERSION}) running on stdio${API_BASE_URL ? `, proxying API at ${API_BASE_URL}` : ''}`);
}
catch (error) {
console.error("Error during server startup:", error);
process.exit(1);
}
}
/**
* Cleanup function for graceful shutdown
*/
async function cleanup() {
console.error("Shutting down MCP server...");
process.exit(0);
}
// Register signal handlers
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
// Start the server
main().catch((error) => {
console.error("Fatal error in main execution:", error);
process.exit(1);
});
/**
* Formats API errors for better readability
*
* @param error Axios error
* @returns Formatted error message
*/
function formatApiError(error) {
let message = 'API request failed.';
if (error.response) {
message = `API Error: Status ${error.response.status} (${error.response.statusText || 'Status text not available'}). `;
const responseData = error.response.data;
const MAX_LEN = 200;
if (typeof responseData === 'string') {
message += `Response: ${responseData.substring(0, MAX_LEN)}${responseData.length > MAX_LEN ? '...' : ''}`;
}
else if (responseData) {
try {
const jsonString = JSON.stringify(responseData);
message += `Response: ${jsonString.substring(0, MAX_LEN)}${jsonString.length > MAX_LEN ? '...' : ''}`;
}
catch {
message += 'Response: [Could not serialize data]';
}
}
else {
message += 'No response body received.';
}
}
else if (error.request) {
message = 'API Network Error: No response received from server.';
if (error.code)
message += ` (Code: ${error.code})`;
}
else {
message += `API Request Setup Error: ${error.message}`;
}
return message;
}
/**
* Converts a JSON Schema to a Zod schema for runtime validation
*
* @param jsonSchema JSON Schema
* @param toolName Tool name for error reporting
* @returns Zod schema
*/
function getZodSchemaFromJsonSchema(jsonSchema, toolName) {
if (typeof jsonSchema !== 'object' || jsonSchema === null) {
return z.object({}).passthrough();
}
try {
const zodSchemaString = jsonSchemaToZod(jsonSchema);
const zodSchema = eval(zodSchemaString);
if (typeof zodSchema?.parse !== 'function') {
throw new Error('Eval did not produce a valid Zod schema.');
}
return zodSchema;
}
catch (err) {
console.error(`Failed to generate/evaluate Zod schema for '${toolName}':`, err);
return z.object({}).passthrough();
}
}
//# sourceMappingURL=index.js.map