Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "parseable",
"displayName": "Parseable",
"version": "0.3.2",
"version": "0.3.3",
"description": "Query and manage Parseable logs, metrics, datasets, and alerts from Cursor.",
"author": {
"name": "Parseable"
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ Scripted:
npx -y @parseable/parseable-mcp-server init \
--client claude-desktop \
--url https://your-parseable.example.com \
--api-key "$PARSEABLE_API_KEY"
--api-key "$PARSEABLE_API_KEY" \
--tool-profile core
```

Supported `--client` values: `claude-desktop`, `cursor`.
Expand Down Expand Up @@ -92,7 +93,7 @@ docker run -p 8787:8787 --env-file .env parseable-mcp-server
- Name: `Parseable`
- URL: `https://mcp.your-domain.com/mcp`
- Header `X-Parseable-URL`: `https://your-parseable.example.com`
- Header `X-API-Key`: your Parseable API key
- Header `Authorization`: `Bearer <your-Parseable-API-key>`
- Optional header `X-Parseable-Tool-Profile`: `core` for only `list_datasets`,
`get_dataset_schema`, and `query_sql`; omit it or use `full` for every tool
- Click Add → Connect
Expand All @@ -102,7 +103,7 @@ docker run -p 8787:8787 --env-file .env parseable-mcp-server
```bash
claude mcp add --transport http parseable https://mcp.your-domain.com/mcp --scope user \
--header "X-Parseable-URL: https://your-parseable.example.com" \
--header "X-API-Key: $PARSEABLE_API_KEY"
--header "Authorization: Bearer $PARSEABLE_API_KEY"
```

### 5. Connect from Cursor / VS Code
Expand All @@ -115,7 +116,7 @@ claude mcp add --transport http parseable https://mcp.your-domain.com/mcp --scop
"url": "https://mcp.your-domain.com/mcp",
"headers": {
"X-Parseable-URL": "https://your-parseable.example.com",
"X-API-Key": "your-parseable-api-key"
"Authorization": "Bearer your-parseable-api-key"
}
}
}
Expand All @@ -126,12 +127,12 @@ claude mcp add --transport http parseable https://mcp.your-domain.com/mcp --scop

## HTTP authentication

Each `POST /mcp` request requires `X-API-Key` and supports two modes:
Each `POST /mcp` request requires `Authorization: Bearer <token>` and supports two modes:

| Mode | Headers |
|------|---------|
| Cloud | `X-Parseable-Mode: cloud`, `X-API-Key` |
| Self-hosted (default) | `X-Parseable-URL`, `X-API-Key` |
| Cloud | `X-Parseable-Mode: cloud`, `Authorization: Bearer <token>` |
| Self-hosted (default) | `X-Parseable-URL`, `Authorization: Bearer <token>` |

`X-Parseable-Tool-Profile` controls tools exposed by HTTP MCP discovery. `core`
exposes only `list_datasets`, `get_dataset_schema`, and `query_sql`. `full`, or
Expand All @@ -154,6 +155,7 @@ For self-hosted mode, HTTP server validates supplied URL and forwards API key to
| `PARSEABLE_URL` | ✅ | — | Parseable base URL |
| `PARSEABLE_API_KEY` | ✅ | — | API key for self-hosted Parseable |
| `PARSEABLE_QUERY_TIMEOUT_MS` | | 30000 | HTTP timeout (ms) |
| `PARSEABLE_TOOL_PROFILE` | | full | Tool catalog profile: `core` or `full` |

### HTTP mode

Expand All @@ -162,7 +164,6 @@ For self-hosted mode, HTTP server validates supplied URL and forwards API key to
| `PORT` | | 8787 | HTTP listen port |
| `PARSEABLE_MCP_ALLOW_PRIVATE` | | false | Permit private/loopback Parseable URLs supplied in request headers |
| `PARSEABLE_ORCHESTRATOR_URL` | Cloud only | - | Parseable Cloud orchestrator base URL |
| `PARSEABLE_CLOUD_AUTH_TOKEN` | Cloud only | - | Service bearer token for API-key validation |
| `PARSEABLE_CLOUD_CACHE_TTL_SECONDS` | | 86400 | Cloud routing LRU TTL |
| `PARSEABLE_CLOUD_CACHE_MAX_ENTRIES` | | 10000 | Maximum cached cloud API-key routes |
| `PARSEABLE_CLOUD_VALIDATE_TIMEOUT_MS` | | 10000 | Cloud validation timeout |
Expand Down Expand Up @@ -301,7 +302,7 @@ CI (GitHub Actions) runs lint + `build:all` + test on every push/PR to `main` on
## Security

- Parseable API keys live in MCP client configuration. Use keys scoped to minimum required permissions.
- HTTP clients send credentials in `X-Parseable-URL` and `X-API-Key`; always use HTTPS for remote deployments.
- HTTP clients send the instance in `X-Parseable-URL` and credentials in `Authorization: Bearer <token>`; always use HTTPS for remote deployments.
- `query_sql` blocks DDL/DML and enforces a row `LIMIT`. Time window is mandatory.
- `evaluate_alert` can fire real notifications — review the call before approving.
- No telemetry. Outbound calls go only to the Parseable instance configured by the user.
Expand Down
2 changes: 1 addition & 1 deletion mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "http",
"url": "https://mcp.parseable.com/mcp",
"headers": {
"X-API-Key": "${env:PARSEABLE_API_KEY}",
"Authorization": "Bearer ${env:PARSEABLE_API_KEY}",
"X-Parseable-Mode": "${env:PARSEABLE_MODE}",
"X-Parseable-URL": "${env:PARSEABLE_URL}"
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@parseable/parseable-mcp-server",
"version": "0.3.2",
"version": "0.3.3",
"mcpName": "io.github.parseablehq/parseable-mcp-server",
"description": "Model Context Protocol server for Parseable. Lets LLMs discover and query Parseable datasets.",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
"url": "https://github.com/parseablehq/parseable-mcp-server",
"source": "github"
},
"version": "0.3.2",
"version": "0.3.3",
"packages": [
{
"registryType": "npm",
"identifier": "@parseable/parseable-mcp-server",
"version": "0.3.2",
"version": "0.3.3",
"transport": {
"type": "stdio"
}
Expand Down
22 changes: 17 additions & 5 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,24 @@ export function header(
return v;
}

function bearerToken(
headers: Headers | Record<string, string | string[] | undefined>,
): string | undefined {
const authorization = header(headers, "Authorization");
if (!authorization) return undefined;
const match = /^Bearer\s+(\S+)$/i.exec(authorization);
if (!match) {
throw new AuthError(401, "Authorization header must use Bearer <token>.");
}
return match[1];
}

export function parseRequestAuth(
headers: Headers | Record<string, string | string[] | undefined>,
): RequestAuth {
const rawMode = header(headers, "X-Parseable-Mode");
const url = header(headers, "X-Parseable-URL");
const apiKey = header(headers, "X-API-Key");
const apiKey = bearerToken(headers);

// Mode is the primary selector. No mode means self-hosted for compatibility.
const mode = rawMode ?? "self-hosted";
Expand All @@ -47,7 +59,7 @@ export function parseRequestAuth(
}

if (!apiKey) {
throw new AuthError(401, "Missing required header: X-API-Key.");
throw new AuthError(401, "Missing required header: Authorization: Bearer <token>.");
}

if (mode === "cloud") {
Expand All @@ -72,16 +84,16 @@ export function parseAuthHeaders(
headers: Headers | Record<string, string | string[] | undefined>,
): RequestCreds {
const url = header(headers, "X-Parseable-URL");
const apiKey = header(headers, "X-API-Key");
const apiKey = bearerToken(headers);

const missing: string[] = [];
if (!url) missing.push("X-Parseable-URL");
if (!apiKey) missing.push("X-API-Key");
if (!apiKey) missing.push("Authorization: Bearer <token>");

if (missing.length) {
throw new AuthError(
401,
`Missing required header(s): ${missing.join(", ")}. Set X-Parseable-URL and X-API-Key on your MCP connector.`,
`Missing required header(s): ${missing.join(", ")}. Set X-Parseable-URL and Authorization: Bearer <token> on your MCP connector.`,
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PARSEABLE_ICON_DATA_URI } from "./icon.js";
import { getRemoteTools } from "./remote-tools.js";
import { filterToolsForProfile, type ToolProfile, toolAllowed } from "./tool-profile.js";

const SERVER_VERSION = "0.3.2";
const SERVER_VERSION = "0.3.3";

function toolError(message: string): CallToolResult {
return { isError: true, content: [{ type: "text", text: message }] };
Expand Down
2 changes: 1 addition & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export class ParseableClient {

private headers(): Record<string, string> {
return {
"X-API-Key": this.apiKey,
Authorization: `Bearer ${this.apiKey}`,
...(this.tenantId ? { "x-p-tenant": this.tenantId } : {}),
"Content-Type": "application/json",
Accept: "application/json",
Expand Down
11 changes: 4 additions & 7 deletions src/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,9 @@ export function cloudCacheKey(apiKey: string): string {
return createHash("sha256").update(apiKey).digest("hex");
}

function requiredCloudConfig(): { baseUrl: string; authToken: string; timeoutMs: number } {
function requiredCloudConfig(): { baseUrl: string; timeoutMs: number } {
const baseUrl = process.env.PARSEABLE_ORCHESTRATOR_URL?.replace(/\/+$/, "");
const authToken = process.env.PARSEABLE_CLOUD_AUTH_TOKEN;
if (!baseUrl || !authToken) {
if (!baseUrl) {
throw new AuthError(503, "Parseable Cloud authentication is not configured.");
}
let parsed: URL;
Expand All @@ -55,7 +54,6 @@ function requiredCloudConfig(): { baseUrl: string; authToken: string; timeoutMs:
}
return {
baseUrl,
authToken,
timeoutMs: positiveInt(process.env.PARSEABLE_CLOUD_VALIDATE_TIMEOUT_MS, DEFAULT_TIMEOUT_MS),
};
}
Expand All @@ -68,13 +66,12 @@ function requiredString(value: unknown, field: string): string {
}

async function fetchCloudRouting(apiKey: string): Promise<CloudRouting> {
const { baseUrl, authToken, timeoutMs } = requiredCloudConfig();
const { baseUrl, timeoutMs } = requiredCloudConfig();
let response: Response;
try {
response = await fetch(`${baseUrl}/api/v1/cli/apikey/validate`, {
headers: {
Authorization: `Bearer ${authToken}`,
"X-API-Key": apiKey,
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
signal: AbortSignal.timeout(timeoutMs),
Expand Down
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import "dotenv/config";
import { parseToolProfile, type ToolProfile } from "./tool-profile.js";

export interface Config {
url: string;
apiKey: string;
tenantId?: string;
mode?: "cloud" | "self-hosted";
queryTimeoutMs: number;
toolProfile: ToolProfile;
}

function required(name: string): string {
Expand All @@ -19,5 +21,6 @@ export function loadConfig(): Config {
url: required("PARSEABLE_URL").replace(/\/+$/, ""),
apiKey: required("PARSEABLE_API_KEY"),
queryTimeoutMs: Number(process.env.PARSEABLE_QUERY_TIMEOUT_MS ?? 30000),
toolProfile: parseToolProfile(process.env.PARSEABLE_TOOL_PROFILE),
};
}
4 changes: 3 additions & 1 deletion src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ HTTP MCP endpoint:
POST /mcp

Required headers for static Parseable credentials:
X-API-Key
Authorization: Bearer <token>
X-Parseable-Mode: cloud (cloud only; omitted means self-hosted)
X-Parseable-URL (required when mode is omitted/self-hosted)

Expand Down Expand Up @@ -131,6 +131,7 @@ async function buildApiKeyClient(reqHeaders: Headers): Promise<{
mode: "cloud",
apiKey: auth.apiKey,
queryTimeoutMs,
toolProfile: "full",
};
return {
client: new ParseableClient(config, {
Expand All @@ -146,6 +147,7 @@ async function buildApiKeyClient(reqHeaders: Headers): Promise<{
apiKey: auth.apiKey,
mode: "self-hosted",
queryTimeoutMs,
toolProfile: "full",
};
return { client: new ParseableClient(config), config };
}
Expand Down
36 changes: 31 additions & 5 deletions src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from
import { homedir, platform } from "node:os";
import { dirname, join } from "node:path";
import inquirer from "inquirer";
import { parseToolProfile, type ToolProfile } from "./tool-profile.js";

export interface ClientTarget {
id: string;
Expand All @@ -16,6 +17,7 @@ export interface InitArgs {
url?: string;
apiKey?: string;
client?: string;
toolProfile?: ToolProfile;
}

export type ParseableMode = "cloud" | "self-hosted";
Expand Down Expand Up @@ -96,6 +98,15 @@ export function parseInitArgs(argv: string[]): InitArgs {
} else if (a === "--url" && argv[i + 1]) args.url = argv[++i];
else if (a === "--api-key" && argv[i + 1]) args.apiKey = argv[++i];
else if (a === "--client" && argv[i + 1]) args.client = argv[++i];
else if (a === "--tool-profile") {
const value = argv[++i];
if (!value) throw new Error('--tool-profile must be either "core" or "full"');
try {
args.toolProfile = parseToolProfile(value);
} catch {
throw new Error('--tool-profile must be either "core" or "full"');
}
}
}
return args;
}
Expand All @@ -106,6 +117,7 @@ export function mergeConfig(
creds: InitCredentials,
clientId?: string,
plat: string = platform(),
toolProfile: ToolProfile = "full",
): Record<string, unknown> {
const stdioEntry = (args: string[], env: Record<string, string>) =>
plat === "win32"
Expand All @@ -123,7 +135,8 @@ export function mergeConfig(
"--header",
"X-Parseable-Mode:cloud",
"--header",
`X-API-Key:\${PARSEABLE_API_KEY}`,
`Authorization:Bearer \${PARSEABLE_API_KEY}`,
...(toolProfile === "core" ? ["--header", "X-Parseable-Tool-Profile:core"] : []),
],
{
PARSEABLE_API_KEY: creds.apiKey,
Expand All @@ -134,20 +147,26 @@ export function mergeConfig(
url: CLOUD_MCP_URL,
headers: {
"X-Parseable-Mode": "cloud",
"X-API-Key": creds.apiKey,
Authorization: `Bearer ${creds.apiKey}`,
...(toolProfile === "core" ? { "X-Parseable-Tool-Profile": "core" } : {}),
},
}
: stdioEntry(["-y", "@parseable/parseable-mcp-server"], {
PARSEABLE_URL: creds.url,
PARSEABLE_API_KEY: creds.apiKey,
...(toolProfile === "core" ? { PARSEABLE_TOOL_PROFILE: "core" } : {}),
});

const servers = (existing[configKey] as Record<string, unknown>) ?? {};
servers.Parseable = entry;
return { ...existing, [configKey]: servers };
}

export function writeClientConfig(target: ClientTarget, creds: InitCredentials): void {
export function writeClientConfig(
target: ClientTarget,
creds: InitCredentials,
toolProfile: ToolProfile = "full",
): void {
let existing: Record<string, unknown> = {};
if (existsSync(target.configPath)) {
try {
Expand All @@ -165,7 +184,14 @@ export function writeClientConfig(target: ClientTarget, creds: InitCredentials):
mkdirSync(dirname(target.configPath), { recursive: true });
}

const merged = mergeConfig(existing, target.configKey, creds, target.id, target.platform);
const merged = mergeConfig(
existing,
target.configKey,
creds,
target.id,
target.platform,
toolProfile,
);
writeFileSync(target.configPath, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
}

Expand Down Expand Up @@ -248,7 +274,7 @@ export async function runInit(argv: string[] = process.argv.slice(3)): Promise<v

// 3. Write
try {
writeClientConfig(selectedTarget, creds);
writeClientConfig(selectedTarget, creds, args.toolProfile ?? "full");
console.log(
`\n✓ Configuration saved to ${selectedTarget.configPath} for ${selectedTarget.name}`,
);
Expand Down
Loading
Loading