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 package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@parseable/parseable-mcp-server",
"version": "0.2.9",
"version": "0.2.10",
"description": "Model Context Protocol server for Parseable. Lets LLMs discover and query Parseable datasets.",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { tools } from "./tools/index.js";
import type { ToolContext } from "./tools/types.js";
import { errorResult, jsonResult } from "./tools/types.js";

const SERVER_VERSION = "0.2.9";
const SERVER_VERSION = "0.2.10";

export function buildMcpServer(ctx: ToolContext): McpServer {
const server = new McpServer({
Expand Down
81 changes: 59 additions & 22 deletions src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,20 @@ export interface ClientTarget {
}

export interface InitArgs {
mode?: ParseableMode;
url?: string;
apiKey?: string;
client?: string;
}

export type ParseableMode = "cloud" | "self-hosted";

export type InitCredentials =
| { mode: "cloud"; apiKey: string }
| { mode: "self-hosted"; url: string; apiKey: string };

const CLOUD_MCP_URL = "https://mcp.parseable.com/mcp";

export function getClientTargets(
home: string = homedir(),
plat: string = platform(),
Expand Down Expand Up @@ -72,7 +81,9 @@ export function parseInitArgs(argv: string[]): InitArgs {
const args: InitArgs = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--url" && argv[i + 1]) args.url = argv[++i];
if (a === "--mode" && (argv[i + 1] === "cloud" || argv[i + 1] === "self-hosted")) {
args.mode = argv[++i] as ParseableMode;
} 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];
}
Expand All @@ -82,23 +93,33 @@ export function parseInitArgs(argv: string[]): InitArgs {
export function mergeConfig(
existing: Record<string, unknown>,
configKey: "mcpServers" | "servers",
creds: { url: string; apiKey: string },
creds: InitCredentials,
): Record<string, unknown> {
const entry = {
command: "npx",
args: ["-y", "@parseable/parseable-mcp-server"],
env: {
PARSEABLE_URL: creds.url,
PARSEABLE_API_KEY: creds.apiKey,
},
};
const entry =
creds.mode === "cloud"
? {
type: "http",
url: CLOUD_MCP_URL,
headers: {
"X-Parseable-Mode": "cloud",
"X-API-Key": creds.apiKey,
},
}
: {
command: "npx",
args: ["-y", "@parseable/parseable-mcp-server"],
env: {
PARSEABLE_URL: creds.url,
PARSEABLE_API_KEY: creds.apiKey,
},
};

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

function writeClientConfig(target: ClientTarget, creds: { url: string; apiKey: string }): void {
function writeClientConfig(target: ClientTarget, creds: InitCredentials): void {
let existing: Record<string, unknown> = {};
if (existsSync(target.configPath)) {
try {
Expand Down Expand Up @@ -154,18 +175,35 @@ export async function runInit(argv: string[] = process.argv.slice(3)): Promise<v
selectedTarget = match;
}

// 2. Credentials
// 2. Mode and credentials
let mode = args.mode;
if (!mode) {
const modeAnswer = await inquirer.prompt<{ mode: ParseableMode }>([
{
type: "select",
name: "mode",
message: "Select deployment mode:",
choices: [
{ name: "Parseable Cloud", value: "cloud" },
{ name: "Self-hosted", value: "self-hosted" },
],
},
]);
mode = modeAnswer.mode;
}

const answers = await inquirer.prompt<{
url: string;
apiKey: string;
}>(
[
!args.url && {
type: "input",
name: "url",
message: "Parseable URL:",
validate: (v: string) => v.trim().length > 0 || "URL is required",
},
mode === "self-hosted" &&
!args.url && {
type: "input",
name: "url",
message: "Parseable URL:",
validate: (v: string) => v.trim().length > 0 || "URL is required",
},
!args.apiKey && {
type: "password",
name: "apiKey",
Expand All @@ -176,10 +214,9 @@ export async function runInit(argv: string[] = process.argv.slice(3)): Promise<v
].filter(Boolean) as Parameters<typeof inquirer.prompt>[0],
);

const creds = {
url: args.url ?? answers.url,
apiKey: args.apiKey ?? answers.apiKey,
};
const apiKey = args.apiKey ?? answers.apiKey;
const creds: InitCredentials =
mode === "cloud" ? { mode, apiKey } : { mode, url: args.url ?? answers.url, apiKey };

// 3. Write
try {
Expand Down
34 changes: 30 additions & 4 deletions test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ import { describe, expect, it } from "vitest";
import { getClientTargets, mergeConfig, parseInitArgs } from "../src/init.js";

describe("parseInitArgs", () => {
it("parses --url --api-key --client", () => {
const a = parseInitArgs(["--url", "http://x", "--api-key", "key", "--client", "cursor"]);
it("parses --mode --url --api-key --client", () => {
const a = parseInitArgs([
"--mode",
"self-hosted",
"--url",
"http://x",
"--api-key",
"key",
"--client",
"cursor",
]);
expect(a).toEqual({
mode: "self-hosted",
url: "http://x",
apiKey: "key",
client: "cursor",
Expand Down Expand Up @@ -78,7 +88,7 @@ describe("getClientTargets", () => {
});

describe("mergeConfig", () => {
const creds = { url: "http://x", apiKey: "key" };
const creds = { mode: "self-hosted" as const, url: "http://x", apiKey: "key" };

it("adds Parseable entry under mcpServers", () => {
const merged = mergeConfig({}, "mcpServers", creds);
Expand All @@ -102,6 +112,22 @@ describe("mergeConfig", () => {
expect(merged.mcpServers).toBeUndefined();
});

it("adds hosted HTTP entry for cloud mode without a Parseable URL", () => {
const merged = mergeConfig({}, "mcpServers", { mode: "cloud", apiKey: "cloud-key" });
expect(merged).toEqual({
mcpServers: {
Parseable: {
type: "http",
url: "https://mcp.parseable.com/mcp",
headers: {
"X-Parseable-Mode": "cloud",
"X-API-Key": "cloud-key",
},
},
},
});
});

it("preserves other top-level keys", () => {
const merged = mergeConfig(
{ mcpServers: {}, preferences: { theme: "dark" } },
Expand All @@ -126,7 +152,7 @@ describe("mergeConfig", () => {
const merged = mergeConfig(
{ mcpServers: { Parseable: { command: "node", args: ["old.js"] } } },
"mcpServers",
{ url: "http://new", apiKey: "key" },
{ mode: "self-hosted", url: "http://new", apiKey: "key" },
);
const servers = merged.mcpServers as Record<string, unknown>;
const parseable = servers.Parseable as { env: Record<string, string> };
Expand Down
Loading