Skip to content

Commit b7c1bb7

Browse files
authored
Merge pull request #9 from speakeasy-api/feat/extensions
feat: extend a server with custom tools
1 parent c0da9f2 commit b7c1bb7

File tree

18 files changed

+1671
-379
lines changed

18 files changed

+1671
-379
lines changed

.changeset/programmatic-api.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@speakeasy-api/docs-mcp-server": minor
3+
---
4+
5+
Add clean programmatic API: `createDocsServer()` factory, `ToolProvider` interface, custom tool support, and Zod-validated options schema

mise.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ depends = ["build"]
1111

1212
[tasks."serve:http"]
1313
description = "Start MCP docs server over Streamable HTTP (set PORT to override)"
14-
run = "node packages/server/dist/bin.js --index-dir tests/fixtures/index --transport http --port ${PORT:-20310} --allow-chunks-fallback"
14+
run = "node packages/server/dist/bin.js --index-dir tests/fixtures/index --transport http --port ${PORT:-20310}"
1515
sources = ["packages/server/src/**"]
1616
depends = ["index-fixtures"]
1717

@@ -35,7 +35,7 @@ depends = ["index-fixtures"]
3535

3636
[tasks.playground]
3737
description = "Start playground (MCP server + Vite dev server)"
38-
run = 'mprocs "node packages/server/dist/bin.js --index-dir tests/fixtures/index --transport http --port ${PORT:-20310} --allow-chunks-fallback" "cd packages/playground && npx vite"'
38+
run = 'mprocs "node packages/server/dist/bin.js --index-dir tests/fixtures/index --transport http --port ${PORT:-20310}" "cd packages/playground && npx vite"'
3939
depends = ["index-fixtures"]
4040

4141
[tasks."playground:server"]

packages/core/scripts/generate-schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ManifestSchema } from "../src/manifest-schema.js";
77
const __dirname = dirname(fileURLToPath(import.meta.url));
88
const outPath = resolve(__dirname, "../../../schemas/docs-mcp.schema.json");
99

10-
const jsonSchema = z.toJSONSchema(ManifestSchema, { target: "draft-2020-12" });
10+
const jsonSchema = z.toJSONSchema(ManifestSchema, { target: "draft-7" });
1111

1212
const content = JSON.stringify(jsonSchema, null, 2) + "\n";
1313
writeFileSync(outPath, content);

packages/core/src/manifest-schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ export const ManifestTaxonomyConfigSchema = z
7070

7171
export const ManifestSchema = z
7272
.object({
73+
$schema: z
74+
.string()
75+
.optional()
76+
.describe(
77+
"JSON Schema URI for editor validation. Typically set automatically by SchemaStore; can be added explicitly.",
78+
),
7379
version: z
7480
.literal("1")
7581
.describe("Schema version. Must be '1'.")

packages/core/test/manifest-schema.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const schemaPath = resolve(__dirname, "../../../schemas/docs-mcp.schema.json");
1111
describe("manifest JSON schema", () => {
1212
it("committed schema matches generated schema from Zod", () => {
1313
const generated = z.toJSONSchema(ManifestSchema, {
14-
target: "draft-2020-12",
14+
target: "draft-7",
1515
});
1616
const committed = JSON.parse(readFileSync(schemaPath, "utf-8"));
1717

packages/server/README.md

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,110 @@ docs-mcp-server --index-dir ./dist/.lancedb --transport http --port 20310
1919
# Stdio transport (for MCP host integration)
2020
docs-mcp-server --index-dir ./dist/.lancedb --transport stdio
2121
```
22-
2322
## Programmatic Usage
2423

24+
### Boot with defaults
25+
26+
```typescript
27+
import { createDocsServer, startStdioServer } from "@speakeasy-api/docs-mcp-server";
28+
29+
const server = await createDocsServer({ indexDir: "./my-index" });
30+
await startStdioServer(server);
31+
```
32+
33+
### Inject a custom tool
34+
35+
```typescript
36+
import { createDocsServer, startStdioServer } from "@speakeasy-api/docs-mcp-server";
37+
38+
const server = await createDocsServer({
39+
indexDir: "./my-index",
40+
customTools: [
41+
{
42+
name: "submit_feedback",
43+
description: "Submit user feedback about a doc page",
44+
inputSchema: {
45+
type: "object",
46+
properties: {
47+
chunk_id: { type: "string" },
48+
rating: { type: "integer", minimum: 1, maximum: 5 }
49+
},
50+
required: ["chunk_id", "rating"]
51+
},
52+
handler: async (args) => {
53+
console.log("Feedback:", args);
54+
return { content: [{ type: "text", text: "Thanks!" }], isError: false };
55+
}
56+
}
57+
]
58+
});
59+
await startStdioServer(server);
60+
```
61+
62+
### Run over HTTP
63+
2564
```typescript
26-
import { McpDocsServer } from "@speakeasy-api/docs-mcp-server";
65+
import { createDocsServer, startHttpServer } from "@speakeasy-api/docs-mcp-server";
66+
67+
const server = await createDocsServer({ indexDir: "./my-index" });
68+
const { port } = await startHttpServer(server, { port: 3000 });
69+
console.log(`Listening on http://localhost:${port}/mcp`);
70+
```
2771

28-
const server = new McpDocsServer({
29-
dbPath: "./dist/.lancedb",
72+
### HTTP authentication
73+
74+
The `authenticate` hook runs before each request. Return `AuthInfo` to attach
75+
caller identity to the request context, or throw to reject with 401.
76+
77+
```typescript
78+
import { createDocsServer, startHttpServer } from "@speakeasy-api/docs-mcp-server";
79+
import type { AuthInfo } from "@speakeasy-api/docs-mcp-server";
80+
81+
const server = await createDocsServer({
82+
indexDir: "./my-index",
83+
customTools: [
84+
{
85+
name: "whoami",
86+
description: "Return the authenticated caller's client ID",
87+
inputSchema: { type: "object", properties: {} },
88+
handler: async (_args, context) => ({
89+
content: [{ type: "text", text: `You are: ${context.authInfo?.clientId ?? "unknown"}` }],
90+
isError: false
91+
})
92+
}
93+
]
3094
});
3195

32-
server.start();
96+
await startHttpServer(server, {
97+
port: 3000,
98+
authenticate: async ({ headers }) => {
99+
const token = (headers.authorization as string | undefined)?.replace("Bearer ", "");
100+
if (!token) throw new Error("Missing bearer token");
101+
// Validate the token and return AuthInfo
102+
return { token, clientId: "my-client", scopes: ["read"] };
103+
}
104+
});
33105
```
34106

107+
Custom tool handlers receive a `ToolCallContext` with `authInfo`, `headers`,
108+
`clientInfo`, and an abort `signal`.
109+
110+
## Option Reference
111+
112+
| Field | Type | Default | Description |
113+
|-------|------|---------|-------------|
114+
| `indexDir` | `string` | *required* | Directory containing `chunks.json` and `metadata.json` from `docs-mcp build`. |
115+
| `toolPrefix` | `string` || Prefix for built-in tool names, e.g. `"acme"``acme_search_docs`. Does not affect custom tool names. Alphanumeric, dash, or underscore. |
116+
| `queryEmbeddingApiKey` | `string` | `OPENAI_API_KEY` env | API key for query-time embeddings. |
117+
| `queryEmbeddingBaseUrl` | `string` | Provider default | Base URL for the embedding API. Defaults to the provider's official endpoint (e.g. `https://api.openai.com/v1` for OpenAI). Override to use a proxy or compatible API. |
118+
| `queryEmbeddingBatchSize` | `number` | `128` | Number of texts per embedding API call. Reduce if hitting provider rate or payload limits. Positive integer. |
119+
| `proximityWeight` | `number` | `1.25` | RRF blend weight for lexical phrase-proximity matches. Higher values boost results where query terms appear close together. Positive. |
120+
| `phraseSlop` | `number` | `0` | Maximum word distance allowed for phrase matches (0 = exact phrase only, up to 5). |
121+
| `vectorWeight` | `number` | `1` | RRF blend weight for vector (semantic) search results. Higher values boost semantically similar results. Positive. |
122+
| `customTools` | `CustomTool[]` | `[]` | Additional tools registered alongside the built-in `search_docs` and `get_doc`. |
123+
124+
The exported `CreateDocsServerOptionsSchema` (Zod) is the canonical machine-readable spec for these options.
125+
35126
## MCP Tools
36127

37128
| Tool | Description |

0 commit comments

Comments
 (0)