diff --git a/CLAUDE.md b/CLAUDE.md index 006bd3b..539b8d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ OST MCP is the Model Context Protocol server for [OpenSourceTogether](https://opensource-together.com/). It lets developers discover and explore open-source projects directly from Claude Desktop, IDEs, and other MCP-compatible clients. -It consumes the OST Linker REST API and exposes 7 MCP tools for project search, discovery, and similarity. +It consumes the OST backend MCP gateway and exposes 7 MCP tools for project search, discovery, and similarity. ## Common Commands @@ -27,11 +27,11 @@ npx vitest --watch # Watch mode ## Architecture ``` -User (Claude Desktop/IDE) -> MCP Server (stdio) -> OSTClient (HTTP) -> OST Linker API +User (Claude Desktop/IDE) -> MCP Server (stdio) -> OSTClient (HTTP) -> OST backend MCP gateway ``` - `src/index.ts` — MCP server entry point, registers all tools -- `src/client.ts` — HTTP client for the OST Linker REST API +- `src/client.ts` — HTTP client for the OST backend MCP gateway - `src/tools/` — One file per MCP tool (or group) - `src/config.ts` — Reads `OST_API_URL` from env - `src/types.ts` — Shared TypeScript types @@ -52,7 +52,8 @@ User (Claude Desktop/IDE) -> MCP Server (stdio) -> OSTClient (HTTP) -> OST Linke | Variable | Purpose | Default | |----------|---------|---------| -| `OST_API_URL` | OST Linker API base URL | `https://api.opensource-together.com` | +| `OST_API_KEY` | Personal Access Token from your OST account | **required** | +| `OST_API_URL` | OST backend MCP gateway base URL | `https://api.opensource-together.com/v1/mcp` | ## Related Repos @@ -70,9 +71,12 @@ Published as `@opensource-together/mcp` on npm. Users install via: "command": "npx", "args": ["@opensource-together/mcp"], "env": { - "OST_API_URL": "https://api.opensource-together.com" + "OST_API_KEY": "your-personal-access-token", + "OST_API_URL": "https://api.opensource-together.com/v1/mcp" } } } } ``` + +> Generate your key at your OpenSource Together account settings. diff --git a/README.md b/README.md index 0c26822..edff8bd 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,38 @@ Add this to your MCP client config: "mcpServers": { "ost": { "command": "npx", - "args": ["@opensource-together/mcp"] + "args": ["@opensource-together/mcp"], + "env": { + "OST_API_KEY": "your-personal-access-token" + } } } } ``` +> Generate your key at your OpenSource Together account settings. + +## Distribution + +Published as `@opensource-together/mcp` on npm. Example client config: + +```json +{ + "mcpServers": { + "ost": { + "command": "npx", + "args": ["@opensource-together/mcp"], + "env": { + "OST_API_KEY": "your-personal-access-token", + "OST_API_URL": "https://api.opensource-together.com/v1/mcp" + } + } + } +} +``` + +> Generate your key at your OpenSource Together account settings. + ## Tools | Tool | What it does | @@ -33,7 +59,7 @@ Add this to your MCP client config: ## How it works -Ask your agent > [@ost-mcp](https://github.com/opensource-together/ost-mcp) > [@ost-linker](https://github.com/opensource-together/ost-linker) > projects found +Ask your agent > [@ost-mcp](https://github.com/opensource-together/ost-mcp) > [@ost-backend](https://github.com/opensource-together/ost-backend) > projects found - *"Find me React projects for e-commerce"* - *"What's trending in open source right now?"* @@ -48,6 +74,13 @@ npm run build npm run dev ``` +## Environment Variables + +| Variable | Purpose | Default | +|------|-------------|---------| +| `OST_API_KEY` | Personal Access Token from your OST account | Required | +| `OST_API_URL` | MCP gateway base URL | `https://api.opensource-together.com/v1/mcp` | + ## License MIT diff --git a/src/client.ts b/src/client.ts index 2444075..cf24c5c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -20,10 +20,27 @@ export class OSTClient { }); if (!response.ok) { const body = await response.json().catch(() => ({})); - throw new Error( - (body as { detail?: string }).detail || - `API error: ${response.status}` - ); + const detail = (body as { detail?: string }).detail; + + if (response.status === 401 || response.status === 403) { + throw new Error( + "Invalid or missing OST_API_KEY. Generate or regenerate a Personal Access Token in your OpenSource Together account settings." + ); + } + + if (response.status === 429) { + const retryAfter = response.headers.get("Retry-After"); + const retryWindow = retryAfter ? `${retryAfter} seconds` : "shortly"; + throw new Error(`Rate limit exceeded. Retry in ${retryWindow}.`); + } + + if (response.status >= 500) { + throw new Error( + `OST backend temporarily unavailable (status ${response.status}). Please try again shortly.` + ); + } + + throw new Error(detail || `API error: ${response.status}`); } return response.json() as Promise; } diff --git a/src/config.ts b/src/config.ts index 4b2e370..8cba816 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,9 +2,15 @@ export interface Config { apiUrl: string; } -const DEFAULT_API_URL = "https://api.opensource-together.com"; +const DEFAULT_API_URL = "https://api.opensource-together.com/v1/mcp"; +const MISSING_API_KEY_ERROR = + "OST_API_KEY is required. Generate a Personal Access Token in your OpenSource Together account settings, then add OST_API_KEY to your MCP client config env block."; export function getConfig(): Config { + if (!process.env.OST_API_KEY) { + throw new Error(MISSING_API_KEY_ERROR); + } + const rawUrl = process.env.OST_API_URL || DEFAULT_API_URL; const apiUrl = rawUrl.endsWith("/") ? rawUrl.slice(0, -1) : rawUrl; diff --git a/tests/client.test.ts b/tests/client.test.ts index 228c50b..8e14e1b 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -43,4 +43,43 @@ describe("OSTClient", () => { await expect(client.getProject("bad-id")).rejects.toThrow("Not found"); }); + + it("maps 401 responses to a human-readable auth error", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 401, + headers: new Headers(), + json: () => Promise.resolve({ detail: "Unauthorized" }), + }); + + await expect(client.getProject("bad-id")).rejects.toThrow( + "Invalid or missing OST_API_KEY. Generate or regenerate a Personal Access Token in your OpenSource Together account settings." + ); + }); + + it("maps 429 responses to a retry-after message", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 429, + headers: new Headers({ "Retry-After": "30" }), + json: () => Promise.resolve({ detail: "Too many requests" }), + }); + + await expect(client.getProject("slow-down")).rejects.toThrow( + "Rate limit exceeded. Retry in 30 seconds." + ); + }); + + it("maps 503 responses to a temporary backend message", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 503, + headers: new Headers(), + json: () => Promise.resolve({ detail: "Service unavailable" }), + }); + + await expect(client.getProject("server-down")).rejects.toThrow( + "OST backend temporarily unavailable (status 503). Please try again shortly." + ); + }); }); diff --git a/tests/config.test.ts b/tests/config.test.ts index fafb82f..5ca1dd3 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -13,20 +13,31 @@ describe("getConfig", () => { }); it("reads OST_API_URL from environment", () => { + process.env.OST_API_KEY = "test-key"; process.env.OST_API_URL = "https://api.example.com"; const config = getConfig(); expect(config.apiUrl).toBe("https://api.example.com"); }); it("uses default URL when OST_API_URL is not set", () => { + process.env.OST_API_KEY = "test-key"; delete process.env.OST_API_URL; const config = getConfig(); - expect(config.apiUrl).toBe("https://api.opensource-together.com"); + expect(config.apiUrl).toBe("https://api.opensource-together.com/v1/mcp"); }); it("strips trailing slash from URL", () => { + process.env.OST_API_KEY = "test-key"; process.env.OST_API_URL = "https://api.example.com/"; const config = getConfig(); expect(config.apiUrl).toBe("https://api.example.com"); }); + + it("throws when OST_API_KEY is not set", () => { + delete process.env.OST_API_KEY; + + expect(() => getConfig()).toThrow( + "OST_API_KEY is required. Generate a Personal Access Token in your OpenSource Together account settings, then add OST_API_KEY to your MCP client config env block." + ); + }); });