From d0a68dae6d4f51c54e955a74ae365d7343724196 Mon Sep 17 00:00:00 2001 From: Felipe Lopez Date: Tue, 1 Jul 2025 21:18:18 +0200 Subject: [PATCH 1/2] feat: Add document management tools and local executable for Vanta MCP - Introduced new tools for managing documents associated with security controls, including: - `get_control_documents`: Lists documents for a specific control. - `get_document_uploads`: Lists uploaded files for a specific document. - `download_document_file`: Downloads a specific file from a document. - Added a local executable wrapper for the Vanta MCP server to facilitate local development. - Updated package.json to include a script for running the local executable. - Enhanced README.md with new document management features. - Updated .gitignore to include generated files and local environment configurations. --- .gitignore | 3 +- README.md | 10 +++ local-vanta-mcp | 4 + package.json | 1 + src/eval/eval.ts | 29 +++++++ src/index.ts | 29 +++++++ src/operations/documents.ts | 155 ++++++++++++++++++++++++++++++++++++ yarn.lock | 52 +++++------- 8 files changed, 249 insertions(+), 34 deletions(-) create mode 100755 local-vanta-mcp create mode 100644 src/operations/documents.ts 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..871a913 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 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== From 404f5bd33c81206f6caafdc55a07f0e7475d36ee Mon Sep 17 00:00:00 2001 From: Felipe Lopez Date: Wed, 2 Jul 2025 07:40:36 +0200 Subject: [PATCH 2/2] feat: Enhance README with local usage instructions for Vanta MCP - Added a new section detailing how to use the Vanta MCP server locally, including the necessary command and environment configuration. - Updated the README.md to improve clarity for developers working with the forked version of the server. --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 871a913..3601808 100644 --- a/README.md +++ b/README.md @@ -67,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`: