diff --git a/package.json b/package.json index b6a48a0..6c97be9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 7b3f1fe..83f564e 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -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({ diff --git a/src/init.ts b/src/init.ts index c017c93..9b050aa 100644 --- a/src/init.ts +++ b/src/init.ts @@ -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(), @@ -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]; } @@ -82,23 +93,33 @@ export function parseInitArgs(argv: string[]): InitArgs { export function mergeConfig( existing: Record, configKey: "mcpServers" | "servers", - creds: { url: string; apiKey: string }, + creds: InitCredentials, ): Record { - 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) ?? {}; 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 = {}; if (existsSync(target.configPath)) { try { @@ -154,18 +175,35 @@ export async function runInit(argv: string[] = process.argv.slice(3)): Promise([ + { + 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", @@ -176,10 +214,9 @@ export async function runInit(argv: string[] = process.argv.slice(3)): Promise[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 { diff --git a/test/init.test.ts b/test/init.test.ts index f0a3d6d..2174e1f 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -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", @@ -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); @@ -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" } }, @@ -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; const parseable = servers.Parseable as { env: Record };