This document explains the structure, conventions, and utilities used in the Vanta MCP Server operations layer. It is intended for developers extending the src/operations/ directory.
- Purpose: Each operations file wraps one or more Vanta API GET endpoints as MCP tools.
- Scope: Operation modules and registered tools.
- Patterns: Consolidated list/get tools, resource-specific routing tools, and specialized tools for unique behaviors (e.g., downloads).
- Automation: Tools are auto-registered through the registry system; common logic lives in
src/operations/common/.
src/operations/
├── README.md # Operations reference (this file)
├── README.proposed.md # Proposal used for the latest refresh
├── index.ts # Barrel export of all operations modules
├── common/
│ ├── descriptions.ts # Reusable parameter descriptions (e.g., DOCUMENT_ID_DESCRIPTION)
│ ├── imports.ts # Barrel import for CallToolResult, Tool, z, utilities, constants
│ └── utils.ts # Shared schema factories, request helpers, response handlers
├── documents.ts # Document tools (consolidated + download)
├── frameworks.ts # Framework tools (consolidated + nested resources)
├── controls.ts
├── discovered-vendors.ts
├── ...
Many resources expose both “list” and “get by ID” behaviors within a single tool. The helper createConsolidatedSchema creates a schema with an optional ID plus pagination fields, and makeConsolidatedRequest routes the request based on the presence of that ID.
Example (frameworks.ts):
const FrameworksInput = createConsolidatedSchema({
paramName: "frameworkId",
description: FRAMEWORK_ID_DESCRIPTION,
resourceName: "framework",
});
export async function frameworks(
args: z.infer<typeof FrameworksInput>,
): Promise<CallToolResult> {
return makeConsolidatedRequest("/v1/frameworks", args, "frameworkId");
}- No ID provided → lists frameworks with pagination.
- ID provided → fetches a specific framework.
Some resources expose additional nested endpoints. These tools accept a required ID plus a discriminator to route to different endpoints.
Example (documents.ts):
const DocumentResourcesInput = z.object({
documentId: z.string().describe(DOCUMENT_ID_DESCRIPTION),
resourceType: z
.enum(["controls", "links", "uploads"])
.describe(
"Type of document resource: 'controls' for associated controls, 'links' for external references, 'uploads' for attached files",
),
...createPaginationSchema().shape,
});
export async function documentResources(
args: z.infer<typeof DocumentResourcesInput>,
): Promise<CallToolResult> {
const { documentId, resourceType, ...params } = args;
const endpoints = {
controls: `/v1/documents/${String(documentId)}/controls`,
links: `/v1/documents/${String(documentId)}/links`,
uploads: `/v1/documents/${String(documentId)}/uploads`,
};
const url = buildUrl(endpoints[resourceType], params);
const response = await makeAuthenticatedRequest(url);
return handleApiResponse(response);
}When behavior diverges from JSON-based responses (e.g., file downloads), tools implement custom response logic.
Example (documents.ts):
const DownloadDocumentFileInput = z.object({
uploadedFileId: z
.string()
.describe(
"Uploaded file ID to download, e.g. 'upload-123' or specific uploaded file identifier",
),
});
export async function downloadDocumentFile(
args: z.infer<typeof DownloadDocumentFileInput>,
): Promise<CallToolResult> {
const url = buildUrl(
`/v1/document-uploads/${String(args.uploadedFileId)}/download`,
);
const response = await makeAuthenticatedRequest(url);
if (!response.ok) {
return handleApiResponse(response);
}
const contentType =
response.headers.get("content-type") ?? "application/octet-stream";
const contentLength = response.headers.get("content-length");
if (
contentType.startsWith("text/") ||
contentType.includes("application/json") ||
contentType.includes("application/xml") ||
contentType.includes("application/javascript") ||
contentType.includes("application/csv") ||
contentType.includes("text/csv")
) {
const textContent = await response.text();
return {
content: [
{
type: "text" as const,
text: `Document File Content (${contentType}):\n\n${textContent}`,
},
],
};
}
return {
content: [
{
type: "text" as const,
text: `Document File Information:\n- Content Type: ${contentType}\n- Content Length: ${contentLength ? `${contentLength} bytes` : "Unknown"}\n- File Type: ${contentType.startsWith("image/") ? "Image" : contentType.startsWith("video/") ? "Video" : contentType.startsWith("audio/") ? "Audio" : contentType.startsWith("application/pdf") ? "PDF Document" : "Binary File"}\n- Upload ID: ${String(args.uploadedFileId)}\n\nNote: This is a binary file. Use appropriate tools to download and process the actual file content.`,
},
],
};
}- Contains reusable strings for parameter descriptions (e.g.,
DOCUMENT_ID_DESCRIPTION,FRAMEWORK_ID_DESCRIPTION). - Promotes consistency and reduces duplication of descriptive text across operation files.
- Re-exports
CallToolResult,Tool,z, schema factories, request helpers, and description constants. - Imported by every operations file so that a single statement brings in all required utilities:
import {
CallToolResult,
Tool,
z,
createConsolidatedSchema,
createPaginationSchema,
makeConsolidatedRequest,
buildUrl,
makeAuthenticatedRequest,
handleApiResponse,
DOCUMENT_ID_DESCRIPTION,
} from "./common/imports.js";Key exports include:
- Schema factories:
createConsolidatedSchema,createPaginationSchema,createIdSchema,createIdWithPaginationSchema,createFilterSchema. - Request helpers:
makeConsolidatedRequest,makePaginatedGetRequest,makeGetByIdRequest,makeSimpleGetRequest. - URL utilities:
buildUrlfor query string construction. - Response utilities:
handleApiResponse,createErrorResponse,createSuccessResponse.
All utilities enforce consistent error handling and response formatting across tools.
Each operations file follows a common structure:
- Imports from
./common/imports.jsfor all dependencies. - Input schemas using schema factories or explicit Zod objects.
- Tool definitions exporting REST-style tool metadata.
- Implementation functions calling Vanta endpoints using utilities.
- Registry export listing every tool/handler pair for automated registration.
Example skeleton:
// 1. Imports
import {
CallToolResult,
Tool,
z,
createConsolidatedSchema,
makeConsolidatedRequest,
buildUrl,
makeAuthenticatedRequest,
handleApiResponse,
} from "./common/imports.js";
// 2. Input Schemas
const ResourceInput = createConsolidatedSchema({
paramName: "resourceId",
description: "Resource ID...",
resourceName: "resource",
});
const ResourceDetailsInput = z.object({
resourceId: z.string().describe("Resource ID..."),
detailType: z.enum(["summary", "history"]),
...createPaginationSchema().shape,
});
// 3. Tool Definitions
export const ResourcesTool: Tool<typeof ResourceInput> = {
name: "resources",
description: "Access resources...",
parameters: ResourceInput,
};
export const ResourceDetailsTool: Tool<typeof ResourceDetailsInput> = {
name: "resource_details",
description: "Access resource details...",
parameters: ResourceDetailsInput,
};
// 4. Implementation Functions
export async function resources(
args: z.infer<typeof ResourceInput>,
): Promise<CallToolResult> {
return makeConsolidatedRequest("/v1/resources", args, "resourceId");
}
export async function resourceDetails(
args: z.infer<typeof ResourceDetailsInput>,
): Promise<CallToolResult> {
const { resourceId, detailType, ...params } = args;
const url = buildUrl(
`/v1/resources/${String(resourceId)}/${detailType}`,
params,
);
const response = await makeAuthenticatedRequest(url);
return handleApiResponse(response);
}
// 5. Registry Export
export default {
tools: [
{ tool: ResourcesTool, handler: resources },
{ tool: ResourceDetailsTool, handler: resourceDetails },
],
};- Tool names: Use plural nouns for consolidated tools (e.g.,
frameworks,documents). - Schema constants: Use PascalCase with
Inputsuffix (e.g.,DocumentsInput). - Implementation functions: Use camelCase matching tool names (e.g.,
frameworks,documentResources). - Registry export: Always include every tool/handler pair in the default export.
- Descriptions: Reference centralized descriptions from
common/descriptions.tswhenever possible.
- Each operations file exports a default object
{ tools: [...] }. src/registry.tsautomatically imports everysrc/operations/*.tsmodule and registers the listed tools (see Step 7 below).
- Create or edit input schemas using factory helpers or explicit
z.objectdefinitions. - Define or update tool metadata with REST-aligned naming.
- Implement handlers using
makeConsolidatedRequest,makePaginatedGetRequest, or custom logic. - Extend the default export with the new tool/handler pair.
- Update
src/operations/index.tsto re-export the module (if a new file is added). - Document new tools in
README.md(root) and update evaluation artifacts (below). - Enable the tool in
src/config.ts. Add the tool's name to theenabledToolNamesarray to make it available through the MCP server. Leaving the array empty enables all tools.
Whenever tools change:
- Update
src/eval/eval.tsto include the new tool definition and test cases. - Update
src/eval/README.mdto describe new or renamed test scenarios.
- TypeScript Build:
npm run build - Linting:
npm run lint -- src/operations/*.ts - Manual Testing: Invoke tools through the MCP interface if available.
- Consolidated tool example:
frameworks.ts(frameworkstool). - Nested resource example:
documents.ts(document_resourcestool). - Download example:
documents.ts(download_document_filetool). - Common utilities:
src/operations/common/utils.ts. - Automated registry:
src/registry.ts+ per-fileexport default { tools: [...] }.
Use this README as the canonical reference for updates to the operations layer. Developers should rely on it when adding, modifying, or auditing tools.