/**
* 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}`);
}
}