@proventools/mcp
Advanced tools
+66
-8
| #!/usr/bin/env node | ||
| const SERVER_VERSION = "0.1.0-beta.1"; | ||
| import { constants as fsConstants } from "node:fs"; | ||
| import { open } from "node:fs/promises"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| const SERVER_VERSION = "0.1.0-beta.2"; | ||
| const PROTOCOL_VERSION = "2024-11-05"; | ||
| const API_KEY_PATTERN = /^pt_live_[a-f0-9]{32}$/; | ||
| const DEFAULT_API_ORIGIN = "https://www.proventools.net"; | ||
@@ -19,2 +25,5 @@ const TRUSTED_API_ORIGINS = new Set([ | ||
| const REQUEST_TIMEOUT_MS = 20_000; | ||
| const MAX_CONFIG_BYTES = 16 * 1024; | ||
| const CONFIG_PATH = path.join(os.homedir(), ".config", "proventools", "config.json"); | ||
| const NOFOLLOW_FLAG = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0; | ||
@@ -303,8 +312,7 @@ const ZERO_CREDIT = "Credit cost: 0. Reads stored ProvenTools data only."; | ||
| function apiKey() { | ||
| const key = (process.env.PROVENTOOLS_API_KEY || "").trim(); | ||
| if (!key) { | ||
| throw new ToolError( | ||
| "PROVENTOOLS_API_KEY is not set — get a key at www.proventools.net/dashboard/api-keys." | ||
| ); | ||
| function validatedApiKey(value) { | ||
| const key = typeof value === "string" ? value.trim() : ""; | ||
| if (!key) return ""; | ||
| if (!API_KEY_PATTERN.test(key)) { | ||
| throw new ToolError("The configured ProvenTools API key has an invalid format."); | ||
| } | ||
@@ -314,2 +322,52 @@ return key; | ||
| async function storedApiKey() { | ||
| let handle; | ||
| try { | ||
| handle = await open(CONFIG_PATH, fsConstants.O_RDONLY | NOFOLLOW_FLAG); | ||
| } catch (error) { | ||
| if (error?.code === "ENOENT") return ""; | ||
| if (error?.code === "ELOOP") { | ||
| throw new ToolError("Refusing to read a symlinked ProvenTools config file."); | ||
| } | ||
| throw new ToolError("Could not open the ProvenTools config file. Run “proventools login” to replace it."); | ||
| } | ||
| try { | ||
| const metadata = await handle.stat(); | ||
| if (!metadata.isFile()) { | ||
| throw new ToolError("Refusing to read an unsafe ProvenTools config file."); | ||
| } | ||
| if (metadata.size > MAX_CONFIG_BYTES) { | ||
| throw new ToolError("Refusing to read an oversized ProvenTools config file."); | ||
| } | ||
| if (process.platform !== "win32" && (metadata.mode & 0o077) !== 0) { | ||
| throw new ToolError( | ||
| "Refusing to read the ProvenTools config because it is accessible by other users. Run “proventools login” to recreate it securely." | ||
| ); | ||
| } | ||
| if (typeof process.getuid === "function" && metadata.uid !== process.getuid()) { | ||
| throw new ToolError("Refusing to read a ProvenTools config owned by another user."); | ||
| } | ||
| try { | ||
| return validatedApiKey(JSON.parse(await handle.readFile("utf8"))?.apiKey); | ||
| } catch (error) { | ||
| if (error instanceof ToolError) throw error; | ||
| throw new ToolError("Could not read the ProvenTools config file. Run “proventools login” to replace it."); | ||
| } | ||
| } finally { | ||
| await handle.close().catch(() => {}); | ||
| } | ||
| } | ||
| async function apiKey() { | ||
| const environmentKey = validatedApiKey(process.env.PROVENTOOLS_API_KEY); | ||
| if (environmentKey) return environmentKey; | ||
| const key = await storedApiKey(); | ||
| if (key) return key; | ||
| throw new ToolError( | ||
| "Not logged in. Run “proventools login” or set PROVENTOOLS_API_KEY." | ||
| ); | ||
| } | ||
| async function readResponseText(response) { | ||
@@ -359,3 +417,3 @@ const declaredLength = Number(response.headers.get("content-length")); | ||
| const headers = { | ||
| Authorization: `Bearer ${apiKey()}`, | ||
| Authorization: `Bearer ${await apiKey()}`, | ||
| Accept: responseType === "text" ? "text/markdown" : "application/json", | ||
@@ -362,0 +420,0 @@ "User-Agent": `proventools-mcp/${SERVER_VERSION}`, |
+1
-1
| { | ||
| "name": "@proventools/mcp", | ||
| "version": "0.1.0-beta.1", | ||
| "version": "0.1.0-beta.2", | ||
| "description": "Read-only local MCP server for the ProvenTools idea library", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+9
-7
@@ -13,5 +13,7 @@ # ProvenTools MCP server | ||
| Make `PROVENTOOLS_API_KEY` available through the MCP client's secret store or | ||
| inherited environment. Never place a literal key in command arguments, shell | ||
| history, checked-in MCP configuration, an agent prompt, or a log. | ||
| Run `proventools login` once to save the key in the private ProvenTools CLI | ||
| config. The MCP server reuses that file and prefers `PROVENTOOLS_API_KEY` when | ||
| your MCP client provides it through a secret store or inherited environment. | ||
| Never place a literal key in command arguments, shell history, checked-in MCP | ||
| configuration, an agent prompt, or a log. | ||
@@ -24,3 +26,3 @@ The examples pin the exact beta version so an agent cannot silently execute a | ||
| ```sh | ||
| codex mcp add proventools -- npx --yes @proventools/mcp@0.1.0-beta.1 | ||
| codex mcp add proventools -- npx --yes @proventools/mcp@0.1.0-beta.2 | ||
| ``` | ||
@@ -31,3 +33,3 @@ | ||
| ```sh | ||
| claude mcp add proventools -- npx --yes @proventools/mcp@0.1.0-beta.1 | ||
| claude mcp add proventools -- npx --yes @proventools/mcp@0.1.0-beta.2 | ||
| ``` | ||
@@ -44,3 +46,3 @@ | ||
| "command": "npx", | ||
| "args": ["--yes", "@proventools/mcp@0.1.0-beta.1"] | ||
| "args": ["--yes", "@proventools/mcp@0.1.0-beta.2"] | ||
| } | ||
@@ -56,3 +58,3 @@ } | ||
| "command": "npx", | ||
| "args": ["--yes", "@proventools/mcp@0.1.0-beta.1"] | ||
| "args": ["--yes", "@proventools/mcp@0.1.0-beta.2"] | ||
| } | ||
@@ -59,0 +61,0 @@ ``` |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances
30293
8.67%749
7.61%98
2.08%5
66.67%