@actalumen/mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # @actalumen/mcp-server
2
+
3
+ MCP server for [ActaLumen](https://actalumen.com) — verify documents for compliance from Claude Desktop, Cursor, and Claude Code.
4
+
5
+ ## What it does
6
+
7
+ Exposes ActaLumen's verification, chat, and document tooling to any MCP-compatible AI agent. Your agent can upload a contract, run it against a compliance template (SOC2, GDPR, custom), and chat with the document — all grounded in citations.
8
+
9
+ **PII is redacted server-side before storage.** Agents never see un-redacted content, even when explicitly asked.
10
+
11
+ ## Install
12
+
13
+ ### Prerequisites
14
+
15
+ 1. An ActaLumen API key with `read`, `write`, and `verify` scopes. Create one at **app.actalumen.com → Settings → API Keys**.
16
+ 2. Node.js 18.17 or newer.
17
+ 3. A directory the agent is allowed to upload from. Default: `~/actalumen-inbox` (create it: `mkdir ~/actalumen-inbox`).
18
+
19
+ ### Claude Desktop
20
+
21
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "actalumen": {
27
+ "command": "npx",
28
+ "args": ["-y", "@actalumen/mcp-server"],
29
+ "env": {
30
+ "ACTALUMEN_API_KEY": "ak_live_..."
31
+ }
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ Restart Claude Desktop. You should see the ActaLumen tools in the tools menu.
38
+
39
+ ### Cursor
40
+
41
+ Add to `~/.cursor/mcp.json`:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "actalumen": {
47
+ "command": "npx",
48
+ "args": ["-y", "@actalumen/mcp-server"],
49
+ "env": { "ACTALUMEN_API_KEY": "ak_live_..." }
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ ### Claude Code
56
+
57
+ ```bash
58
+ claude mcp add actalumen -- npx -y @actalumen/mcp-server
59
+ # then set the key in your shell or settings:
60
+ export ACTALUMEN_API_KEY=ak_live_...
61
+ ```
62
+
63
+ ## Configuration
64
+
65
+ | Env var | Default | Purpose |
66
+ |---|---|---|
67
+ | `ACTALUMEN_API_KEY` | (required) | Org-scoped API key. |
68
+ | `ACTALUMEN_BASE_URL` | `https://api.actalumen.com` | Override for self-hosted or staging. |
69
+ | `ACTALUMEN_UPLOAD_DIR` | `~/actalumen-inbox` | Path allowlist — agents can only upload files from inside this directory. |
70
+
71
+ ## Tools
72
+
73
+ | Tool | Purpose |
74
+ |---|---|
75
+ | `upload_document` | Upload a PDF (must be inside `ACTALUMEN_UPLOAD_DIR`). Returns a document ID; PII is redacted on the server before storage. |
76
+ | `get_document` | Poll a document's status — wait for `READY` before verifying or chatting. |
77
+ | `list_documents` | Find existing documents by name without re-uploading. |
78
+ | `list_templates` | Discover available compliance templates (e.g., SOC2 Vendor). |
79
+ | `start_verification` | Run a document against a template. Returns a `jobId`. |
80
+ | `get_verification` | Fetch report status and findings. |
81
+ | `chat_with_document` | Ask grounded questions about one or more documents. |
82
+ | `get_usage` | Check plan quota before batch operations. |
83
+
84
+ ## Example agent flow
85
+
86
+ > "Check whether `~/actalumen-inbox/acme-msa.pdf` meets our SOC2 vendor requirements and summarize the gaps."
87
+
88
+ The agent will:
89
+
90
+ 1. `list_templates` → find the SOC2 template.
91
+ 2. `upload_document` → get a document ID, wait for `READY` via `get_document`.
92
+ 3. `start_verification` → get a `jobId`.
93
+ 4. `get_verification` (polling) → receive the full report.
94
+ 5. Summarize the failed criteria back to you with page citations.
95
+
96
+ ## Verify your install
97
+
98
+ ```bash
99
+ ACTALUMEN_API_KEY=ak_live_... npx -y @actalumen/mcp-server --health
100
+ # → ok — https://api.actalumen.com — org Acme Inc — upload dir /Users/.../actalumen-inbox
101
+ ```
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1,16 @@
1
+ import type { Config } from "./config.js";
2
+ export declare class ApiError extends Error {
3
+ status: number;
4
+ body: unknown;
5
+ constructor(status: number, body: unknown, message: string);
6
+ }
7
+ export declare class ActalumenClient {
8
+ private readonly cfg;
9
+ constructor(cfg: Config);
10
+ private headers;
11
+ private parse;
12
+ private request;
13
+ get<T>(path: string, query?: Record<string, string | number | undefined>): Promise<T>;
14
+ post<T>(path: string, body: unknown): Promise<T>;
15
+ postMultipart<T>(path: string, form: FormData): Promise<T>;
16
+ }
package/dist/client.js ADDED
@@ -0,0 +1,66 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ body;
4
+ constructor(status, body, message) {
5
+ super(message);
6
+ this.status = status;
7
+ this.body = body;
8
+ this.name = "ApiError";
9
+ }
10
+ }
11
+ export class ActalumenClient {
12
+ cfg;
13
+ constructor(cfg) {
14
+ this.cfg = cfg;
15
+ }
16
+ headers(extra = {}) {
17
+ return {
18
+ "X-API-Key": this.cfg.apiKey,
19
+ "User-Agent": "actalumen-mcp/0.1.0",
20
+ ...extra,
21
+ };
22
+ }
23
+ async parse(res) {
24
+ const text = await res.text();
25
+ if (!text)
26
+ return null;
27
+ try {
28
+ return JSON.parse(text);
29
+ }
30
+ catch {
31
+ return text;
32
+ }
33
+ }
34
+ async request(method, path, init = {}) {
35
+ const url = new URL(this.cfg.baseUrl + path);
36
+ if (init.query) {
37
+ for (const [k, v] of Object.entries(init.query)) {
38
+ if (v !== undefined)
39
+ url.searchParams.set(k, String(v));
40
+ }
41
+ }
42
+ const res = await fetch(url, {
43
+ method,
44
+ headers: this.headers(init.headers),
45
+ body: init.body,
46
+ });
47
+ const body = await this.parse(res);
48
+ if (!res.ok) {
49
+ const detail = typeof body === "object" && body && "error" in body ? String(body.error) : res.statusText;
50
+ throw new ApiError(res.status, body, `${method} ${path} → ${res.status}: ${detail}`);
51
+ }
52
+ return body;
53
+ }
54
+ get(path, query) {
55
+ return this.request("GET", path, { query });
56
+ }
57
+ post(path, body) {
58
+ return this.request("POST", path, {
59
+ body: JSON.stringify(body),
60
+ headers: { "Content-Type": "application/json" },
61
+ });
62
+ }
63
+ postMultipart(path, form) {
64
+ return this.request("POST", path, { body: form });
65
+ }
66
+ }
@@ -0,0 +1,6 @@
1
+ export interface Config {
2
+ apiKey: string;
3
+ baseUrl: string;
4
+ uploadDir: string;
5
+ }
6
+ export declare function loadConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,11 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ export function loadConfig() {
4
+ const apiKey = process.env.ACTALUMEN_API_KEY;
5
+ if (!apiKey) {
6
+ throw new Error("ACTALUMEN_API_KEY is not set. Create a key at https://app.actalumen.com/settings/api-keys");
7
+ }
8
+ const baseUrl = (process.env.ACTALUMEN_BASE_URL ?? "https://api.actalumen.com").replace(/\/+$/, "");
9
+ const uploadDir = path.resolve(process.env.ACTALUMEN_UPLOAD_DIR ?? path.join(os.homedir(), "actalumen-inbox"));
10
+ return { apiKey, baseUrl, uploadDir };
11
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { zodToJsonSchema } from "zod-to-json-schema";
6
+ import { loadConfig } from "./config.js";
7
+ import { ActalumenClient, ApiError } from "./client.js";
8
+ import { tools } from "./tools/index.js";
9
+ async function runHealthCheck() {
10
+ try {
11
+ const cfg = loadConfig();
12
+ const client = new ActalumenClient(cfg);
13
+ const me = await client.get("/v1/me");
14
+ process.stdout.write(`ok — ${cfg.baseUrl} — org ${me.orgName ?? me.orgId ?? "(unknown)"} — upload dir ${cfg.uploadDir}\n`);
15
+ return 0;
16
+ }
17
+ catch (err) {
18
+ const msg = err instanceof Error ? err.message : String(err);
19
+ process.stderr.write(`health check failed: ${msg}\n`);
20
+ return 1;
21
+ }
22
+ }
23
+ async function main() {
24
+ if (process.argv.includes("--health")) {
25
+ process.exit(await runHealthCheck());
26
+ }
27
+ const config = loadConfig();
28
+ const client = new ActalumenClient(config);
29
+ const ctx = { client, config };
30
+ const server = new Server({ name: "actalumen", version: "0.1.0" }, { capabilities: { tools: {} } });
31
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
32
+ tools: tools.map((t) => ({
33
+ name: t.name,
34
+ description: t.description,
35
+ inputSchema: zodToJsonSchema(t.inputSchema),
36
+ })),
37
+ }));
38
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
39
+ const tool = tools.find((t) => t.name === req.params.name);
40
+ if (!tool) {
41
+ return {
42
+ isError: true,
43
+ content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }],
44
+ };
45
+ }
46
+ const parsed = tool.inputSchema.safeParse(req.params.arguments ?? {});
47
+ if (!parsed.success) {
48
+ return {
49
+ isError: true,
50
+ content: [{ type: "text", text: `Invalid arguments: ${parsed.error.message}` }],
51
+ };
52
+ }
53
+ try {
54
+ const result = await tool.handler(parsed.data, ctx);
55
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
56
+ }
57
+ catch (err) {
58
+ const text = err instanceof ApiError
59
+ ? `ActaLumen API error (${err.status}): ${JSON.stringify(err.body)}`
60
+ : err instanceof Error
61
+ ? err.message
62
+ : String(err);
63
+ return { isError: true, content: [{ type: "text", text }] };
64
+ }
65
+ });
66
+ const transport = new StdioServerTransport();
67
+ await server.connect(transport);
68
+ process.stderr.write(`ActaLumen MCP ready — ${config.baseUrl} — upload dir ${config.uploadDir}\n`);
69
+ }
70
+ main().catch((err) => {
71
+ process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}\n`);
72
+ process.exit(1);
73
+ });
@@ -0,0 +1 @@
1
+ export declare const chatWithDocument: import("./types.js").ToolDef;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "./types.js";
3
+ const Input = z.object({
4
+ documentIds: z
5
+ .array(z.string())
6
+ .min(1)
7
+ .describe("One or more document IDs to ground the answer in. All must be READY."),
8
+ message: z.string().min(1).describe("The user's question. Answers are grounded in retrieved chunks with page citations."),
9
+ sessionId: z
10
+ .string()
11
+ .optional()
12
+ .describe("Optional existing chat session ID to continue a multi-turn conversation."),
13
+ });
14
+ export const chatWithDocument = defineTool({
15
+ name: "chat_with_document",
16
+ description: "Ask a grounded question about one or more documents. Returns an answer with citations into the document. PII in source documents has already been redacted server-side, so answers cannot leak redacted data.",
17
+ inputSchema: Input,
18
+ handler: async (input, { client }) => client.post("/v1/chat", input),
19
+ });
@@ -0,0 +1 @@
1
+ export declare const getDocument: import("./types.js").ToolDef;
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "./types.js";
3
+ const Input = z.object({
4
+ documentId: z.string().describe("Document ID returned by upload_document."),
5
+ });
6
+ export const getDocument = defineTool({
7
+ name: "get_document",
8
+ description: "Get a document's status and metadata. Use this to poll after upload — call start_verification or chat_with_document only when status is READY. Status values: PROCESSING (embeddings running), READY (queryable), FAILED.",
9
+ inputSchema: Input,
10
+ handler: async ({ documentId }, { client }) => client.get(`/v1/documents/${encodeURIComponent(documentId)}`),
11
+ });
@@ -0,0 +1 @@
1
+ export declare const getUsage: import("./types.js").ToolDef;
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "./types.js";
3
+ const Input = z.object({}).describe("No arguments.");
4
+ export const getUsage = defineTool({
5
+ name: "get_usage",
6
+ description: "Check the organization's current plan, quota, and remaining allowance for documents, verifications, chat messages, and generated media. Call before kicking off expensive batch operations.",
7
+ inputSchema: Input,
8
+ handler: async (_input, { client }) => client.get("/v1/usage"),
9
+ });
@@ -0,0 +1 @@
1
+ export declare const getVerification: import("./types.js").ToolDef;
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "./types.js";
3
+ const Input = z.object({
4
+ jobId: z.string().describe("Job ID returned by start_verification."),
5
+ });
6
+ export const getVerification = defineTool({
7
+ name: "get_verification",
8
+ description: "Fetch verification job status and (when complete) the full report with per-criterion findings, citations, and pass/fail summary. Poll roughly every 5–10s after start_verification.",
9
+ inputSchema: Input,
10
+ handler: async ({ jobId }, { client }) => client.get(`/v1/reports/${encodeURIComponent(jobId)}`),
11
+ });
@@ -0,0 +1,2 @@
1
+ import type { ToolDef } from "./types.js";
2
+ export declare const tools: ToolDef[];
@@ -0,0 +1,18 @@
1
+ import { uploadDocument } from "./upload_document.js";
2
+ import { getDocument } from "./get_document.js";
3
+ import { listDocuments } from "./list_documents.js";
4
+ import { listTemplates } from "./list_templates.js";
5
+ import { startVerification } from "./start_verification.js";
6
+ import { getVerification } from "./get_verification.js";
7
+ import { chatWithDocument } from "./chat_with_document.js";
8
+ import { getUsage } from "./get_usage.js";
9
+ export const tools = [
10
+ uploadDocument,
11
+ getDocument,
12
+ listDocuments,
13
+ listTemplates,
14
+ startVerification,
15
+ getVerification,
16
+ chatWithDocument,
17
+ getUsage,
18
+ ];
@@ -0,0 +1 @@
1
+ export declare const listDocuments: import("./types.js").ToolDef;
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "./types.js";
3
+ const Input = z.object({
4
+ limit: z.number().int().min(1).max(100).optional().describe("Max documents to return (default 25)."),
5
+ offset: z.number().int().min(0).optional(),
6
+ });
7
+ export const listDocuments = defineTool({
8
+ name: "list_documents",
9
+ description: "List documents in the organization. Use this to find an existing document ID by name instead of re-uploading.",
10
+ inputSchema: Input,
11
+ handler: async (input, { client }) => client.get("/v1/documents", input),
12
+ });
@@ -0,0 +1 @@
1
+ export declare const listTemplates: import("./types.js").ToolDef;
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "./types.js";
3
+ const Input = z.object({}).describe("No arguments.");
4
+ export const listTemplates = defineTool({
5
+ name: "list_templates",
6
+ description: "List compliance criteria templates available to the organization (e.g., 'SOC2 Vendor', 'GDPR DPA'). Call this before start_verification to find the right templateId. Each template encodes a checklist of criteria a document is verified against.",
7
+ inputSchema: Input,
8
+ handler: async (_input, { client }) => client.get("/v1/templates"),
9
+ });
@@ -0,0 +1 @@
1
+ export declare const startVerification: import("./types.js").ToolDef;
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "./types.js";
3
+ const Input = z
4
+ .object({
5
+ documentId: z.string().describe("Document to verify. Must be READY (see get_document)."),
6
+ templateId: z
7
+ .number()
8
+ .int()
9
+ .optional()
10
+ .describe("Template ID from list_templates. Either templateId or criteria is required."),
11
+ criteria: z
12
+ .array(z.object({ id: z.string(), text: z.string() }))
13
+ .optional()
14
+ .describe("Ad-hoc criteria list, used when no template fits. Either templateId or criteria is required."),
15
+ })
16
+ .refine((v) => v.templateId !== undefined || (v.criteria && v.criteria.length > 0), {
17
+ message: "Provide either templateId or a non-empty criteria array.",
18
+ });
19
+ export const startVerification = defineTool({
20
+ name: "start_verification",
21
+ description: "Run compliance verification on a document against a template (or ad-hoc criteria). Returns a jobId; the job runs async — poll get_verification until status is 'completed'. Requires the API key to have the 'verify' scope.",
22
+ inputSchema: Input,
23
+ handler: async (input, { client }) => client.post("/v1/reports", input),
24
+ });
@@ -0,0 +1,19 @@
1
+ import type { z } from "zod";
2
+ import type { ActalumenClient } from "../client.js";
3
+ import type { Config } from "../config.js";
4
+ export interface ToolContext {
5
+ client: ActalumenClient;
6
+ config: Config;
7
+ }
8
+ export interface ToolDef {
9
+ name: string;
10
+ description: string;
11
+ inputSchema: z.ZodType<unknown>;
12
+ handler: (input: unknown, ctx: ToolContext) => Promise<unknown>;
13
+ }
14
+ export declare function defineTool<Input extends z.ZodTypeAny>(t: {
15
+ name: string;
16
+ description: string;
17
+ inputSchema: Input;
18
+ handler: (input: z.infer<Input>, ctx: ToolContext) => Promise<unknown>;
19
+ }): ToolDef;
@@ -0,0 +1,3 @@
1
+ export function defineTool(t) {
2
+ return t;
3
+ }
@@ -0,0 +1 @@
1
+ export declare const uploadDocument: import("./types.js").ToolDef;
@@ -0,0 +1,45 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { defineTool } from "./types.js";
5
+ const Input = z.object({
6
+ path: z
7
+ .string()
8
+ .describe("Absolute or ~/-relative path to a PDF. Must be inside ACTALUMEN_UPLOAD_DIR (default ~/actalumen-inbox) — the user controls which files agents can upload."),
9
+ id: z
10
+ .string()
11
+ .regex(/^[a-zA-Z0-9_-]{1,128}$/)
12
+ .optional()
13
+ .describe("Optional custom document ID (letters, digits, -, _, max 128). Useful for idempotent re-runs."),
14
+ });
15
+ function expandHome(p) {
16
+ if (p.startsWith("~/"))
17
+ return path.join(process.env.HOME ?? "", p.slice(2));
18
+ return p;
19
+ }
20
+ export const uploadDocument = defineTool({
21
+ name: "upload_document",
22
+ description: "Upload a PDF to ActaLumen for compliance verification. The server applies organization-configured PII redaction before storage — the agent will only ever see redacted content in subsequent calls. Returns a document ID; the document is PROCESSING until embeddings finish (poll get_document for status=READY).",
23
+ inputSchema: Input,
24
+ handler: async (input, { client, config }) => {
25
+ const resolved = path.resolve(expandHome(input.path));
26
+ const allowed = config.uploadDir;
27
+ const rel = path.relative(allowed, resolved);
28
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
29
+ throw new Error(`Upload denied: ${resolved} is outside the allowed upload directory (${allowed}). ` +
30
+ `Move the file there, or set ACTALUMEN_UPLOAD_DIR to a directory containing it.`);
31
+ }
32
+ const stat = await fs.stat(resolved);
33
+ if (!stat.isFile())
34
+ throw new Error(`Not a file: ${resolved}`);
35
+ if (!resolved.toLowerCase().endsWith(".pdf")) {
36
+ throw new Error("Only PDF files are supported.");
37
+ }
38
+ const bytes = await fs.readFile(resolved);
39
+ const form = new FormData();
40
+ form.append("file", new Blob([bytes], { type: "application/pdf" }), path.basename(resolved));
41
+ if (input.id)
42
+ form.append("id", input.id);
43
+ return client.postMultipart("/v1/documents", form);
44
+ },
45
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@actalumen/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for ActaLumen — verify documents for compliance from Claude Desktop, Cursor, and Claude Code.",
5
+ "type": "module",
6
+ "bin": {
7
+ "actalumen-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsc --watch",
16
+ "start": "node dist/index.js",
17
+ "health": "node dist/index.js --health",
18
+ "test": "vitest run"
19
+ },
20
+ "engines": {
21
+ "node": ">=18.17"
22
+ },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.0.4",
25
+ "zod": "^3.23.8",
26
+ "zod-to-json-schema": "^3.23.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^20.14.0",
30
+ "typescript": "^5.5.0",
31
+ "vitest": "^2.0.0"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }