diff --git a/.gitignore b/.gitignore index 6d20071..190aea1 100644 --- a/.gitignore +++ b/.gitignore @@ -139,4 +139,5 @@ node_modules/ build/ # Claude Code generated files -CLAUDE.md \ No newline at end of file +CLAUDE.md +.vanta_env diff --git a/README.md b/README.md index 0587d37..3601808 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,13 @@ A Model Context Protocol serve - Get specific tests that validate each security control - Understand which automated tests monitor compliance for specific controls +### Document Management + +- Access documents associated with specific security controls for evidence tracking +- List uploaded files and attachments for compliance documentation +- Download specific document files with proper authentication +- Track document upload status and file metadata for audit trails + ### Multi-Region Support - US, EU, and AUS regions with region-specific API endpoints @@ -41,6 +48,9 @@ A Model Context Protocol serve | `get_framework_controls` | Get detailed security control requirements for a specific compliance framework. Returns the specific controls, their descriptions, implementation guidance, and current compliance status. Essential for understanding what security measures are required for each compliance standard. | | `get_controls` | List all security controls across all frameworks in your Vanta account. Returns control names, descriptions, framework mappings, and current implementation status. Use this to see all available controls or to find a specific control ID for use with other tools. | | `get_control_tests` | Get all automated tests that validate a specific security control. Use this when you know a control ID and want to see which specific tests monitor compliance for that control. Returns test details, current status, and any failing entities for the control's tests. | +| `get_control_documents` | List all documents associated with a specific security control. Returns document details including names, types, and upload status for evidence and documentation linked to the control. | +| `get_document_uploads` | List all uploaded files for a specific document. Returns file details including names, upload dates, and file IDs for files that have been uploaded to provide evidence for a document. | +| `download_document_file` | Download a specific file from a document. Provides access to the actual file content when you know both the document ID and uploaded file ID. Returns download information and URL for authenticated access. | ## Configuration @@ -57,6 +67,19 @@ A Model Context Protocol serve > **Note:** Vanta currently only allows a single active access_token per Application today. [More info here](https://developer.vanta.com/docs/api-access-setup#authentication-and-token-retrieval) +### Usage locally + +as this is a fork, you need to build the server and use it from local: +```json +"localVanta": { + "command": "/path_to/vanta-mcp-server/local-vanta-mcp", + "env": { + "VANTA_ENV_FILE": "/path_to/.vanta_env" + } + }, +``` + + ### Usage with Claude Desktop Add the server to your `claude_desktop_config.json`: diff --git a/local-vanta-mcp b/local-vanta-mcp new file mode 100755 index 0000000..0a6300a --- /dev/null +++ b/local-vanta-mcp @@ -0,0 +1,4 @@ +#!/usr/bin/env node + +// Local executable wrapper for Vanta MCP Server +import('./build/index.js'); \ No newline at end of file diff --git a/package.json b/package.json index 5b02fc9..c163f79 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "scripts": { "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", "start": "yarn build && node build/index.js", + "local-vanta-mcp": "yarn build && node build/index.js", "eval": "tsc && node build/eval/eval.js", "lint": "eslint .", "lint:fix": "eslint . --fix", diff --git a/src/eval/eval.ts b/src/eval/eval.ts index 649ec6b..87a81b5 100644 --- a/src/eval/eval.ts +++ b/src/eval/eval.ts @@ -9,6 +9,11 @@ import { GetControlsTool, GetControlTestsTool, } from "../operations/controls.js"; +import { + GetControlDocumentsTool, + GetDocumentUploadsTool, + DownloadDocumentFileTool, +} from "../operations/documents.js"; // Format all tools for OpenAI const tools = [ @@ -60,6 +65,30 @@ const tools = [ parameters: zodToJsonSchema(GetControlTestsTool.parameters), }, }, + { + type: "function" as const, + function: { + name: GetControlDocumentsTool.name, + description: GetControlDocumentsTool.description, + parameters: zodToJsonSchema(GetControlDocumentsTool.parameters), + }, + }, + { + type: "function" as const, + function: { + name: GetDocumentUploadsTool.name, + description: GetDocumentUploadsTool.description, + parameters: zodToJsonSchema(GetDocumentUploadsTool.parameters), + }, + }, + { + type: "function" as const, + function: { + name: DownloadDocumentFileTool.name, + description: DownloadDocumentFileTool.description, + parameters: zodToJsonSchema(DownloadDocumentFileTool.parameters), + }, + }, ]; // Test cases with expected tool calls diff --git a/src/index.ts b/src/index.ts index eaaa4d1..ae26ca3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,14 @@ import { getControls, getControlTests, } from "./operations/controls.js"; +import { + GetControlDocumentsTool, + GetDocumentUploadsTool, + DownloadDocumentFileTool, + getControlDocuments, + getDocumentUploads, + downloadDocumentFile, +} from "./operations/documents.js"; import { initializeToken } from "./auth.js"; const server = new McpServer({ @@ -71,6 +79,27 @@ server.tool( getControlTests, ); +server.tool( + GetControlDocumentsTool.name, + GetControlDocumentsTool.description, + GetControlDocumentsTool.parameters.shape, + getControlDocuments, +); + +server.tool( + GetDocumentUploadsTool.name, + GetDocumentUploadsTool.description, + GetDocumentUploadsTool.parameters.shape, + getDocumentUploads, +); + +server.tool( + DownloadDocumentFileTool.name, + DownloadDocumentFileTool.description, + DownloadDocumentFileTool.parameters.shape, + downloadDocumentFile, +); + async function main() { try { await initializeToken(); diff --git a/src/operations/documents.ts b/src/operations/documents.ts new file mode 100644 index 0000000..039dd1b --- /dev/null +++ b/src/operations/documents.ts @@ -0,0 +1,155 @@ +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { baseApiUrl } from "../api.js"; +import { Tool } from "../types.js"; +import { z } from "zod"; +import { makeAuthenticatedRequest } from "./utils.js"; + +const GetControlDocumentsInput = z.object({ + controlId: z + .string() + .describe("The ID of the control to list documents for"), + pageSize: z + .number() + .describe("Number of documents to return (1-100, default 10)") + .optional(), + pageCursor: z.string().describe("Pagination cursor for next page").optional(), +}); + +export const GetControlDocumentsTool: Tool = { + name: "get_control_documents", + description: + "List all documents associated with a specific security control. Use this when you know a control ID and want to see which documents provide evidence or documentation for that control. Returns document details including names, types, and upload status.", + parameters: GetControlDocumentsInput, +}; + +const GetDocumentUploadsInput = z.object({ + documentId: z + .string() + .describe("The ID of the document to list uploaded files for"), + pageSize: z + .number() + .describe("Number of uploads to return (1-100, default 10)") + .optional(), + pageCursor: z.string().describe("Pagination cursor for next page").optional(), +}); + +export const GetDocumentUploadsTool: Tool = { + name: "get_document_uploads", + description: + "List all uploaded files for a specific document. Use this when you know a document ID and want to see which files have been uploaded to provide evidence for that document. Returns file details including names, upload dates, and file IDs.", + parameters: GetDocumentUploadsInput, +}; + +const DownloadDocumentFileInput = z.object({ + documentId: z + .string() + .describe("The ID of the document containing the file"), + uploadedFileId: z + .string() + .describe("The ID of the specific uploaded file to download"), +}); + +export const DownloadDocumentFileTool: Tool = { + name: "download_document_file", + description: + "Download a specific file from a document. Use this when you know both the document ID and the uploaded file ID and want to retrieve the actual file content. Returns the file content as binary data.", + parameters: DownloadDocumentFileInput, +}; + +export async function getControlDocuments( + args: z.infer, +): Promise { + const url = new URL( + `/v1/controls/${args.controlId}/documents`, + baseApiUrl(), + ); + if (args.pageSize !== undefined) { + url.searchParams.append("pageSize", args.pageSize.toString()); + } + if (args.pageCursor !== undefined) { + url.searchParams.append("pageCursor", args.pageCursor); + } + + const response = await makeAuthenticatedRequest(url.toString()); + if (!response.ok) { + return { + content: [ + { type: "text" as const, text: `Error: ${response.statusText}` }, + ], + }; + } + + return { + content: [ + { type: "text" as const, text: JSON.stringify(await response.json()) }, + ], + }; +} + +export async function getDocumentUploads( + args: z.infer, +): Promise { + const url = new URL( + `/v1/documents/${args.documentId}/uploads`, + baseApiUrl(), + ); + if (args.pageSize !== undefined) { + url.searchParams.append("pageSize", args.pageSize.toString()); + } + if (args.pageCursor !== undefined) { + url.searchParams.append("pageCursor", args.pageCursor); + } + + const response = await makeAuthenticatedRequest(url.toString()); + if (!response.ok) { + return { + content: [ + { type: "text" as const, text: `Error: ${response.statusText}` }, + ], + }; + } + + return { + content: [ + { type: "text" as const, text: JSON.stringify(await response.json()) }, + ], + }; +} + +export async function downloadDocumentFile( + args: z.infer, +): Promise { + const url = new URL( + `/v1/documents/${args.documentId}/uploads/${args.uploadedFileId}/media`, + baseApiUrl(), + ); + + const response = await makeAuthenticatedRequest(url.toString()); + if (!response.ok) { + return { + content: [ + { type: "text" as const, text: `Error: ${response.statusText}` }, + ], + }; + } + + // For file downloads, we'll return information about the file rather than binary content + // since MCP tools typically work with text/JSON responses + const contentType = response.headers.get("content-type") || "unknown"; + const contentLength = response.headers.get("content-length") || "unknown"; + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ + message: "File download endpoint accessed successfully", + contentType, + contentLength, + downloadUrl: url.toString(), + note: "Use this URL with proper authentication to download the actual file content" + }) + }, + ], + }; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 236c64e..85a8897 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,7 +29,7 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@^8.57.0", "@eslint/js@8.57.1": +"@eslint/js@8.57.1", "@eslint/js@^8.57.0": version "8.57.1" resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== @@ -77,7 +77,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -127,7 +127,7 @@ natural-compare "^1.4.0" ts-api-utils "^1.3.0" -"@typescript-eslint/parser@^7.0.0", "@typescript-eslint/parser@^7.2.0": +"@typescript-eslint/parser@^7.2.0": version "7.18.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz" integrity sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg== @@ -218,7 +218,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.9.0: +acorn@^8.9.0: version "8.14.1" resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz" integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== @@ -309,7 +309,7 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" -bytes@^3.1.2, bytes@3.1.2: +bytes@3.1.2, bytes@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== @@ -423,7 +423,7 @@ delayed-stream@~1.0.0: resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -depd@^2.0.0, depd@2.0.0: +depd@2.0.0, depd@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -516,7 +516,7 @@ eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -"eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@^8.56.0, eslint@^8.57.0, eslint@>=7.0.0: +eslint@^8.57.0: version "8.57.1" resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== @@ -620,7 +620,7 @@ express-rate-limit@^7.5.0: resolved "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz" integrity sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg== -"express@^4.11 || 5 || ^5.0.0-beta.1", express@^5.0.1: +express@^5.0.1: version "5.1.0" resolved "https://registry.npmjs.org/express/-/express-5.1.0.tgz" integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA== @@ -880,7 +880,7 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" -http-errors@^2.0.0, http-errors@2.0.0: +http-errors@2.0.0, http-errors@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== @@ -898,7 +898,7 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -iconv-lite@^0.6.3, iconv-lite@0.6.3: +iconv-lite@0.6.3, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -1050,16 +1050,16 @@ micromatch@^4.0.8: braces "^3.0.3" picomatch "^2.3.1" -mime-db@^1.54.0: - version "1.54.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz" - integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== - mime-db@1.52.0: version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + mime-types@^2.1.12: version "2.1.35" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" @@ -1074,21 +1074,7 @@ mime-types@^3.0.0, mime-types@^3.0.1: dependencies: mime-db "^1.54.0" -minimatch@^3.0.5: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^3.1.2: +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -1428,7 +1414,7 @@ slash@^3.0.0: resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -statuses@^2.0.1, statuses@2.0.1: +statuses@2.0.1, statuses@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== @@ -1500,7 +1486,7 @@ type-is@^2.0.0, type-is@^2.0.1: media-typer "^1.1.0" mime-types "^3.0.0" -typescript@^5.8.2, typescript@>=4.2.0: +typescript@^5.8.2: version "5.8.2" resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz" integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ== @@ -1577,7 +1563,7 @@ zod-to-json-schema@^3.24.1: resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz" integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g== -zod@^3.23.8, zod@^3.24.1, "zod@>= 3": +"zod@>= 3", zod@^3.23.8: version "3.24.2" resolved "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz" integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==