Skip to main content
Glama
index.ts19.3 kB
/** * MCP Tools index - registers all tools with the MCP server. */ import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { z } from "zod"; import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { join, relative, resolve } from "node:path"; import { syncSpec, searchOperations, describeOperation, getOperations, type ApiType, type EnvType, } from "../lib/spec.js"; import { executeApiCall, describeOperationParams } from "../lib/api.js"; import { getToken, type TokenResult } from "../lib/keychain.js"; import { getOnboardedRepoPath } from "../lib/config.js"; import { putEntity, queryEntities, getEntityById, getEntityByExternalId, deleteEntity, countEntities, type Entity, } from "../lib/state.js"; // Input schemas for tool validation const ApiTypeSchema = z.enum(["v1", "internal"]); const EnvTypeSchema = z.enum(["prod", "staging"]); // Tool definitions export const toolDefinitions = [ { name: "spec_sync", description: "Fetch and cache an OpenAPI spec from the Onboarded API. Must be called before using other operations.", inputSchema: { type: "object" as const, properties: { api: { type: "string", enum: ["v1", "internal"], description: "Which API spec to sync", }, env: { type: "string", enum: ["prod", "staging"], description: "Environment (default: prod)", }, source: { type: "string", enum: ["url", "cache"], description: "Where to load from (default: url)", }, }, required: ["api"], }, }, { name: "ops_search", description: "Search for API operations by keyword. Returns matching operationIds with summaries.", inputSchema: { type: "object" as const, properties: { api: { type: "string", enum: ["v1", "internal"], description: "Which API to search", }, query: { type: "string", description: "Search query (matches operationId, summary, tags, path)", }, env: { type: "string", enum: ["prod", "staging"], description: "Environment (default: prod)", }, topK: { type: "number", description: "Max results to return (default: 10)", }, }, required: ["api", "query"], }, }, { name: "ops_describe", description: "Get full details for an API operation including parameters and request body schema.", inputSchema: { type: "object" as const, properties: { api: { type: "string", enum: ["v1", "internal"], description: "Which API", }, operationId: { type: "string", description: "The operationId to describe", }, env: { type: "string", enum: ["prod", "staging"], description: "Environment (default: prod)", }, }, required: ["api", "operationId"], }, }, { name: "api_call", description: "Execute an API call. Requires spec_sync to have been called first.", inputSchema: { type: "object" as const, properties: { api: { type: "string", enum: ["v1", "internal"], description: "Which API", }, operationId: { type: "string", description: "The operationId to call", }, env: { type: "string", enum: ["prod", "staging"], description: "Environment (default: prod)", }, params: { type: "object", description: "Path and query parameters", }, body: { type: "object", description: "Request body (for POST/PUT/PATCH)", }, profile: { type: "string", description: "Auth profile to use (default: default)", }, dryRun: { type: "boolean", description: "If true, return the request that would be made without executing", }, }, required: ["api", "operationId"], }, }, { name: "auth_status", description: "Check if authentication credentials are available.", inputSchema: { type: "object" as const, properties: { profile: { type: "string", description: "Profile name (default: default)", }, }, required: [], }, }, { name: "state_put", description: "Store an entity reference (e.g., after creating via API).", inputSchema: { type: "object" as const, properties: { entityType: { type: "string", description: "Type of entity (e.g., 'employee', 'company')", }, externalId: { type: "string", description: "ID from the Onboarded API", }, name: { type: "string", description: "Human-readable name", }, data: { type: "object", description: "Additional data to store", }, api: { type: "string", enum: ["v1", "internal"], description: "Which API this entity belongs to", }, env: { type: "string", enum: ["prod", "staging"], description: "Environment (default: prod)", }, }, required: ["entityType", "api"], }, }, { name: "state_query", description: "Query stored entities with filters.", inputSchema: { type: "object" as const, properties: { entityType: { type: "string", description: "Filter by entity type", }, api: { type: "string", enum: ["v1", "internal"], description: "Filter by API", }, env: { type: "string", enum: ["prod", "staging"], description: "Filter by environment", }, nameContains: { type: "string", description: "Filter by name substring", }, limit: { type: "number", description: "Max results (default: 100)", }, offset: { type: "number", description: "Pagination offset", }, }, required: [], }, }, { name: "state_get", description: "Get a stored entity by ID (internal or external).", inputSchema: { type: "object" as const, properties: { id: { type: "string", description: "Internal ID", }, externalId: { type: "string", description: "External ID from Onboarded API", }, api: { type: "string", enum: ["v1", "internal"], description: "API (required if using externalId)", }, env: { type: "string", enum: ["prod", "staging"], description: "Environment (required if using externalId)", }, }, required: [], }, }, { name: "state_delete", description: "Delete a stored entity by internal ID.", inputSchema: { type: "object" as const, properties: { id: { type: "string", description: "Internal ID to delete", }, }, required: ["id"], }, }, { name: "repo_read", description: "Read a file from the local Onboarded repository. Requires onboardedRepoPath to be configured.", inputSchema: { type: "object" as const, properties: { path: { type: "string", description: "Relative path within the repo (e.g., 'src/models/employee.py')", }, startLine: { type: "number", description: "Start line number (1-indexed, optional)", }, endLine: { type: "number", description: "End line number (1-indexed, optional)", }, }, required: ["path"], }, }, { name: "repo_list", description: "List files in a directory of the local Onboarded repository. Requires onboardedRepoPath to be configured.", inputSchema: { type: "object" as const, properties: { path: { type: "string", description: "Relative path within the repo (e.g., 'src/models'). Empty or '.' for root.", }, recursive: { type: "boolean", description: "If true, list files recursively (default: false)", }, maxDepth: { type: "number", description: "Max depth for recursive listing (default: 3)", }, }, required: [], }, }, ]; // Tool handlers export async function handleToolCall( name: string, args: Record<string, unknown> ): Promise<{ content: Array<{ type: "text"; text: string }> }> { try { const result = await executeToolCall(name, args); return { content: [ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } async function executeToolCall( name: string, args: Record<string, unknown> ): Promise<unknown> { switch (name) { case "spec_sync": { const api = ApiTypeSchema.parse(args.api); const env = args.env ? EnvTypeSchema.parse(args.env) : "prod"; const source = (args.source as "url" | "cache") ?? "url"; const result = await syncSpec(api, env, source); return { success: true, api, env, operationCount: result.operationCount, fromCache: result.fromCache, specVersion: result.spec.info.version, specTitle: result.spec.info.title, }; } case "ops_search": { const api = ApiTypeSchema.parse(args.api); const query = z.string().parse(args.query); const env = args.env ? EnvTypeSchema.parse(args.env) : "prod"; const topK = (args.topK as number) ?? 10; const operations = searchOperations(api, query, env, topK); if (operations.length === 0) { // Check if spec is loaded const allOps = getOperations(api, env); if (allOps.length === 0) { return { success: false, error: `No operations found. Did you run spec_sync for ${api} (${env})?`, }; } return { success: true, matches: [], message: `No operations matching '${query}'`, }; } return { success: true, matches: operations.map((op) => ({ operationId: op.operationId, method: op.method, path: op.path, summary: op.summary, tags: op.tags, })), }; } case "ops_describe": { const api = ApiTypeSchema.parse(args.api); const operationId = z.string().parse(args.operationId); const env = args.env ? EnvTypeSchema.parse(args.env) : "prod"; const details = describeOperation(api, operationId, env); if (!details) { return { success: false, error: `Operation '${operationId}' not found in ${api} (${env})`, }; } // Get formatted description const description = describeOperationParams(api, operationId, env); return { success: true, operation: { operationId: details.operation.operationId, method: details.operation.method, path: details.operation.path, summary: details.operation.summary, description: details.operation.description, tags: details.operation.tags, }, parameters: details.parameters, requestBodySchema: details.requestBodySchema, responseSchema: details.responseSchema, formattedDescription: description, }; } case "api_call": { const api = ApiTypeSchema.parse(args.api); const operationId = z.string().parse(args.operationId); const env = args.env ? EnvTypeSchema.parse(args.env) : "prod"; const params = (args.params as Record<string, unknown>) ?? {}; const body = args.body; const profile = args.profile as string | undefined; const dryRun = (args.dryRun as boolean) ?? false; const result = await executeApiCall({ api, env, operationId, params, body, profile, dryRun, }); return result; } case "auth_status": { const profile = (args.profile as string) ?? "default"; const result = getToken({ profile }); return { success: true, authenticated: result.found, profile: result.profile, error: result.error, }; } case "state_put": { const entityType = z.string().parse(args.entityType); const api = ApiTypeSchema.parse(args.api); const env = args.env ? EnvTypeSchema.parse(args.env) : "prod"; const entity = putEntity({ entityType, externalId: args.externalId as string | undefined, name: args.name as string | undefined, data: args.data as Record<string, unknown> | undefined, api, env, }); return { success: true, entity, }; } case "state_query": { const entities = queryEntities({ entityType: args.entityType as string | undefined, api: args.api as string | undefined, env: args.env as string | undefined, nameContains: args.nameContains as string | undefined, limit: args.limit as number | undefined, offset: args.offset as number | undefined, }); const total = countEntities( args.entityType as string | undefined, args.api as string | undefined, args.env as string | undefined ); return { success: true, entities, count: entities.length, total, }; } case "state_get": { let entity: Entity | null = null; if (args.id) { entity = getEntityById(args.id as string); } else if (args.externalId) { entity = getEntityByExternalId( args.externalId as string, args.api as string | undefined, args.env as string | undefined ); } else { return { success: false, error: "Must provide either 'id' or 'externalId'", }; } if (!entity) { return { success: false, error: "Entity not found", }; } return { success: true, entity, }; } case "state_delete": { const id = z.string().parse(args.id); const deleted = deleteEntity(id); return { success: deleted, message: deleted ? "Entity deleted" : "Entity not found", }; } case "repo_read": { const repoPath = getOnboardedRepoPath(); if (!repoPath) { return { success: false, error: "onboardedRepoPath not configured. Set it in ~/.config/onboarded-mcp/config.json", }; } if (!existsSync(repoPath)) { return { success: false, error: `Repo path does not exist: ${repoPath}`, }; } const filePath = z.string().parse(args.path); const fullPath = resolve(repoPath, filePath); // Security: ensure path is within repo if (!fullPath.startsWith(resolve(repoPath))) { return { success: false, error: "Path traversal not allowed", }; } if (!existsSync(fullPath)) { return { success: false, error: `File not found: ${filePath}`, }; } const stat = statSync(fullPath); if (!stat.isFile()) { return { success: false, error: `Not a file: ${filePath}`, }; } const content = readFileSync(fullPath, "utf-8"); const lines = content.split("\n"); const startLine = (args.startLine as number) ?? 1; const endLine = (args.endLine as number) ?? lines.length; const selectedLines = lines.slice(startLine - 1, endLine); return { success: true, path: filePath, totalLines: lines.length, startLine, endLine: Math.min(endLine, lines.length), content: selectedLines.join("\n"), }; } case "repo_list": { const repoPath = getOnboardedRepoPath(); if (!repoPath) { return { success: false, error: "onboardedRepoPath not configured. Set it in ~/.config/onboarded-mcp/config.json", }; } if (!existsSync(repoPath)) { return { success: false, error: `Repo path does not exist: ${repoPath}`, }; } const dirPath = (args.path as string) ?? "."; const fullPath = resolve(repoPath, dirPath); // Security: ensure path is within repo if (!fullPath.startsWith(resolve(repoPath))) { return { success: false, error: "Path traversal not allowed", }; } if (!existsSync(fullPath)) { return { success: false, error: `Directory not found: ${dirPath}`, }; } const stat = statSync(fullPath); if (!stat.isDirectory()) { return { success: false, error: `Not a directory: ${dirPath}`, }; } const recursive = (args.recursive as boolean) ?? false; const maxDepth = (args.maxDepth as number) ?? 3; const files: Array<{ path: string; type: "file" | "directory" }> = []; const repoPathResolved = resolve(repoPath); function listDir(dir: string, depth: number) { if (depth > maxDepth) return; const entries = readdirSync(dir); for (const entry of entries) { // Skip hidden files and common non-essential directories if ( entry.startsWith(".") || entry === "node_modules" || entry === "__pycache__" || entry === "venv" || entry === ".git" ) { continue; } const entryPath = join(dir, entry); const relativePath = relative(repoPathResolved, entryPath); const entryStat = statSync(entryPath); if (entryStat.isDirectory()) { files.push({ path: relativePath, type: "directory" }); if (recursive) { listDir(entryPath, depth + 1); } } else if (entryStat.isFile()) { files.push({ path: relativePath, type: "file" }); } } } listDir(fullPath, 1); return { success: true, path: dirPath, files, count: files.length, }; } default: throw new Error(`Unknown tool: ${name}`); } }

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/OnboardedInc/onboarded-mcp'

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