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
14 changes: 9 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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.
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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?"*
Expand All @@ -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
25 changes: 21 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>;
}
Expand Down
8 changes: 7 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
39 changes: 39 additions & 0 deletions tests/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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."
);
});
});
13 changes: 12 additions & 1 deletion tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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."
);
});
});
Loading