docs: document inline server setup

This commit is contained in:
Peter Steinberger 2025-11-05 06:11:46 +00:00
parent 3aa0c0b1ac
commit c8404d3872
5 changed files with 123 additions and 111 deletions

View File

@ -56,6 +56,33 @@ const result = await callOnce({
});
```
## Configuration Sources
`mcp-runtime` automatically merges server definitions from multiple tools so your CLI, local IDEs, and agents stay in sync. The default priority is:
1. `config/mcp_servers.json` in your project
2. Project-scoped `.mcp.json` (Claude Code)
3. Project-scoped `.cursor/mcp.json`
4. Project-scoped `.claude/mcp.json`
5. Claude Desktop config (`claude_desktop_config.json`)
6. Cursor user config (`~/.cursor/mcp.json` or OS equivalent)
7. Codex `~/.codex/config.toml`
Sources earlier in the list win conflicts. Create `config/mcp_sources.json` to customize the order or add additional files:
```json
{
"strategy": "last-wins",
"sources": [
{ "kind": "codex" },
{ "kind": "cursor-user" },
{ "kind": "local-json", "path": "./config/mcp_servers.json" }
]
}
```
Supported `kind` values are `local-json`, `project-mcp-json`, `cursor-project`, `cursor-user`, `claude-project`, `claude-user`, `claude-desktop`, and `codex`. Every entry may override `path` and set `optional: false` when a file must exist. Combine this with environment-variable headers (for example `CONTEXT7_API_KEY`) to keep secrets out of source control while still allowing IDE-specific configs to extend the same runtime.
## CLI Reference
```
@ -108,8 +135,24 @@ Prefer the `createServerProxy` helper when you want an ergonomic proxy object fo
```ts
import { createRuntime, createServerProxy } from "mcp-runtime";
const runtime = await createRuntime();
const context7 = createServerProxy(runtime, "context7");
const mcpRuntime = await createRuntime({
servers: [
{
name: "context7",
description: "Context7 docs MCP",
command: {
kind: "http",
url: new URL("https://mcp.context7.com/mcp"),
headers: process.env.CONTEXT7_API_KEY
? { Authorization: `Bearer ${process.env.CONTEXT7_API_KEY}` }
: undefined,
},
},
],
});
// Inline definitions work at runtime; move this block to config/mcp_servers.json if you prefer static config.
const context7 = createServerProxy(mcpRuntime, "context7");
const search = await context7.resolveLibraryId("react");
const docs = await context7.getLibraryDocs("react"); // maps to required schema fields
@ -117,7 +160,7 @@ const docs = await context7.getLibraryDocs("react"); // maps to required schema
console.log(search.text()); // "Available Libraries ..."
console.log(docs.markdown()); // markdown excerpt
await runtime.close();
await mcpRuntime.close();
```
Every property access maps from camelCase to the underlying tool name automatically (`resolveLibraryId` → `resolve-library-id`). Beyond method names, the proxy:
@ -128,7 +171,7 @@ Every property access maps from camelCase to the underlying tool name automatica
- accepts primitives, tuples, or plain objects and routes them onto required schema fields in order (multi-argument tools like Firecrawls `scrape` work with positional calls);
```ts
const firecrawl = createServerProxy(runtime, "firecrawl");
const firecrawl = createServerProxy(mcpRuntime, "firecrawl");
await firecrawl.firecrawlScrape(
"https://example.com/docs",
["markdown", "html"], // 2nd required/optional field from schema
@ -141,21 +184,28 @@ You can still drop down to `context7.call("resolve-library-id", { args: { ... }
### High-level helpers
Some servers benefit from composable workflows. `createContext7Client` layers on top of the proxy to resolve an ID and fetch docs in one call:
You can compose higher-level helpers yourself with plain JavaScript. Because the proxy already maps positional arguments to schema fields, a tiny wrapper is enough to resolve a Context7 library ID and fetch the docs:
```ts
import { createContext7Client, createRuntime } from "mcp-runtime";
const context7 = createServerProxy(mcpRuntime, "context7");
const runtime = await createRuntime();
const context7 = createContext7Client(runtime);
const docs = await context7.getDocs("react"); // resolves ID + downloads markdown
console.log(docs.markdown());
await runtime.close();
async function getContext7Docs(libraryName: string) {
const resolution = await context7.resolveLibraryId(libraryName);
const id =
resolution.json<{ candidates?: Array<{ context7CompatibleLibraryID?: string }> }>()
?.candidates?.find((candidate) => candidate?.context7CompatibleLibraryID)
?.context7CompatibleLibraryID ??
resolution
.text()
?.match(/Context7-compatible library ID:\s*([^\s]+)/)?.[1];
if (!id) {
throw new Error(`Context7 library "${libraryName}" not found.`);
}
return context7.getLibraryDocs(id);
}
```
The helper still returns a `CallResult`, so you can opt into `.json()` / `.text()` as needed.
The returned value is still a `CallResult`, so you can opt into `.markdown()`, `.json()`, etc.
## Testing & CI

View File

@ -29,6 +29,7 @@ summary: 'Plan for the mcp-runtime package replacing the Sweetistics pnpm MCP he
- Tool signature + schema fetching for `list`.
- Provide lazy connection pooling per server to minimize startup cost.
- Expose a lightweight server proxy (`createServerProxy`) that maps camelCase method accesses to tool names, fills JSON-schema defaults, validates required arguments, and returns a helper (`CallResult`) for extracting text/markdown/JSON without re-parsing the content envelope.
- Document direct (inline) server definitions passed to `createRuntime({ servers: [...] })`, including env-sourced headers, so agents can bootstrap without touching config files.
## Schema-Aware Proxy Strategy
- Cache tool schemas on first access; tolerate failures by falling back to raw `callTool`.
@ -39,7 +40,7 @@ summary: 'Plan for the mcp-runtime package replacing the Sweetistics pnpm MCP he
- Merging JSON-schema defaults and validating required fields before dispatch.
- Return `CallResult` objects exposing `.raw`, `.text()`, `.markdown()`, `.json()` helpers for consistent post-processing.
- Keep implementation generic—no hardcoded knowledge of specific servers—so new MCP servers automatically gain the ergonomic API.
- Surface composability helpers (e.g., `createContext7Client`) that compose multiple proxy calls while still delegating schema parsing to the proxy.
- Encourage lightweight composition helpers in examples (e.g., resolving then fetching Context7 docs) while keeping library exports generic.
- Back the proxy with targeted unit tests that cover primitive-only calls, positional tuples + option bags, and error fallbacks when schemas are missing.
## Work Phases

View File

@ -5,15 +5,38 @@
* and print only the markdown headlines.
*/
import { createContext7Client, createRuntime, type CallResult } from "../src/index.js";
import { createRuntime, createServerProxy, type CallResult } from "../src/index.js";
async function main(): Promise<void> {
const runtime = await createRuntime();
const apiKey = process.env.CONTEXT7_API_KEY;
const context7Definition = {
name: "context7",
description: "Context7 documentation MCP",
command: {
kind: "http" as const,
url: new URL("https://mcp.context7.com/mcp"),
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
},
};
// Inline definitions can also live in config/mcp_servers.json if you prefer shared config.
const mcpRuntime = await createRuntime({ servers: [context7Definition] });
try {
const context7 = createContext7Client(runtime);
const docs = await (context7.getDocs as (name: string) => Promise<CallResult>)(
"react",
);
const context7 = createServerProxy(mcpRuntime, "context7") as Record<string, unknown>;
const resolveLibraryId = context7.resolveLibraryId as (
args: unknown,
) => Promise<CallResult>;
const getLibraryDocs = context7.getLibraryDocs as (
args: unknown,
) => Promise<CallResult>;
const resolved = await resolveLibraryId("react");
const contextId = extractContext7LibraryId(resolved);
if (!contextId) {
throw new Error("Unable to resolve React documentation ID from Context7.");
}
const docs = await getLibraryDocs(contextId);
const markdown = docs.markdown() ?? docs.text() ?? "";
const headlines = markdown
@ -24,7 +47,7 @@ async function main(): Promise<void> {
console.log("# Headlines for React");
console.log(headlines || "(no headlines found)");
} finally {
await runtime.close();
await mcpRuntime.close();
}
}
@ -32,3 +55,18 @@ main().catch((error) => {
console.error(error);
process.exit(1);
});
function extractContext7LibraryId(result: CallResult): string | null {
const json = result.json<
{ candidates?: Array<{ context7CompatibleLibraryID?: string }> } | undefined
>();
if (json && json.candidates) {
for (const candidate of json.candidates) {
if (candidate?.context7CompatibleLibraryID) {
return candidate.context7CompatibleLibraryID;
}
}
}
const textMatch = result.text()?.match(/Context7-compatible library ID:\s*([^\s]+)/);
return textMatch?.[1] ?? null;
}

View File

@ -1,86 +0,0 @@
import type { Runtime } from "./runtime.js";
import { createServerProxy } from "./server-proxy.js";
import type { CallResult } from "./result-utils.js";
export interface GetDocsArgs {
libraryName?: string;
libraryId?: string;
docArgs?: Record<string, unknown>;
}
function extractFirstLibraryId(result: CallResult): string | null {
const json = result.json<
|
{ candidates?: Array<{ context7CompatibleLibraryID?: string }> }
|
{ results?: Array<{ id?: string }> }
>();
if (json) {
if (Array.isArray((json as { candidates?: unknown }).candidates)) {
for (const candidate of (json as {
candidates: Array<{ context7CompatibleLibraryID?: string }>;
}).candidates) {
if (candidate?.context7CompatibleLibraryID) {
return candidate.context7CompatibleLibraryID;
}
}
}
if (Array.isArray((json as { results?: unknown }).results)) {
for (const candidate of (json as { results: Array<{ id?: string }> }).results) {
if (candidate?.id) {
return candidate.id;
}
}
}
}
const text = result.text();
if (!text) return null;
const match = text.match(/Context7-compatible library ID:\s*([^\s]+)/);
return match?.[1] ?? null;
}
export function createContext7Client(runtime: Runtime): Record<string, unknown> {
const proxy = createServerProxy(runtime, "context7") as Record<string, unknown>;
const resolveLibraryId = proxy.resolveLibraryId as (
args: unknown,
) => Promise<CallResult>;
const getLibraryDocs = proxy.getLibraryDocs as (
args: unknown,
) => Promise<CallResult>;
const getDocs = async (input: string | GetDocsArgs): Promise<CallResult> => {
const { libraryName, libraryId: providedId, docArgs } =
typeof input === "string"
? { libraryName: input, libraryId: undefined, docArgs: undefined }
: input;
let libraryId = providedId;
if (!libraryId) {
if (!libraryName) {
throw new Error("libraryName is required when libraryId is not provided");
}
const resolved = await resolveLibraryId({ libraryName });
libraryId = extractFirstLibraryId(resolved);
if (!libraryId) {
throw new Error(
`Unable to resolve Context7 library ID for "${libraryName}"`,
);
}
}
const args: Record<string, unknown> = {
context7CompatibleLibraryID: libraryId,
...(docArgs ?? {}),
};
return getLibraryDocs(args);
};
return {
...proxy,
getDocs,
resolveLibraryId,
getLibraryDocs,
};
}

View File

@ -127,9 +127,11 @@ describe("mcp-runtime composability", () => {
servers: [
{
name: "fake",
description: "Inline fake server",
command: {
kind: "http" as const,
url: new URL("https://example.com"),
headers: { Authorization: "Bearer inline-test" },
},
},
],
@ -146,6 +148,16 @@ describe("mcp-runtime composability", () => {
]);
expect(mocks.connectMock).toHaveBeenCalledTimes(1);
expect(mocks.clientInstances).toHaveLength(1);
const streamableTransport = mocks.streamableInstances[0] as {
options?: {
requestInit?: { headers?: Record<string, string> };
authProvider?: unknown;
};
close: ReturnType<typeof vi.fn>;
};
expect(streamableTransport.options?.requestInit?.headers).toEqual({
Authorization: "Bearer inline-test",
});
const first = await runtime.callTool("fake", "echo", {
args: { text: "hi" },
@ -170,9 +182,6 @@ describe("mcp-runtime composability", () => {
await runtime.close();
expect(mocks.streamableInstances).toHaveLength(1);
const streamableTransport = mocks.streamableInstances[0] as {
close: ReturnType<typeof vi.fn>;
};
expect(streamableTransport.close).toHaveBeenCalledTimes(1);
});
});