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
13 changes: 13 additions & 0 deletions .changeset/some-news-say.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@speakeasy-api/docs-mcp-playground": minor
"@speakeasy-api/docs-mcp-server": minor
---

Added `GET /healthz` endpoint to server and support for passing build info to server and playground. On the server this build info is included in every http response in the `DOCS-MCP` header and the response body of `GET /healthz`.

Environment variables are:

- `SERVER_NAME`: the name of the MCP server
- `SERVER_VERSION`: the version of the MCP server
- `GIT_COMMIT`: the git commit hash of the current build
- `BUILD_DATE`: the date of the current build
3 changes: 3 additions & 0 deletions packages/playground/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ Then open `http://localhost:3001` in your browser.
| `MCP_TARGET` | `http://localhost:20310` | URL of the docs-mcp-server to proxy MCP requests to |
| `PLAYGROUND_PASSWORD` | _(none)_ | If set, enables password authentication |
| `SERVER_NAME` | `speakeasy-docs` | Display name shown in the playground UI |
| `SERVER_VERSION` | _(none)_ | Optional version label shown next to server name |
| `GIT_COMMIT` | _(none)_ | Optional commit SHA shown in muted metadata |
| `BUILD_DATE` | _(none)_ | Optional build date shown in muted metadata |

## License

Expand Down
13 changes: 13 additions & 0 deletions packages/playground/src/Playground.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import { Chat, ElementsConfig, GramElementsProvider } from "@gram-ai/elements";
import { ServerUrl } from "./components/ServerUrl.js";
import { Footer } from "./components/Footer.js";
import { InstallMethods } from "./components/InstallMethods.js";
import { ToolsList } from "./components/ToolsList.js";
import { ResourcesList } from "./components/ResourcesList.js";
Expand All @@ -18,6 +19,9 @@ export default function Playground() {
const mcpUrl = `${window.location.origin}/mcp`;
const [token, setToken] = useState<string | undefined>();
const [serverName, setServerName] = useState<string | undefined>();
const [serverVersion, setServerVersion] = useState<string | undefined>();
const [serverCommit, setServerCommit] = useState<string | undefined>();
const [serverBuildDate, setServerBuildDate] = useState<string | undefined>();
const [chatEnabled, setChatEnabled] = useState(false);

useEffect(() => {
Expand All @@ -26,6 +30,9 @@ export default function Playground() {
.then((data) => {
setToken(data.token);
if (data.serverName) setServerName(data.serverName);
if (data.serverVersion) setServerVersion(data.serverVersion);
if (data.serverCommit) setServerCommit(data.serverCommit);
if (data.serverBuildDate) setServerBuildDate(data.serverBuildDate);
setChatEnabled(!!data.chatEnabled);
})
.catch(() => {});
Expand All @@ -37,6 +44,12 @@ export default function Playground() {
<InstallMethods serverUrl={mcpUrl} serverName={serverName} token={token} />
<ToolsList chatEnabled={chatEnabled} />
<ResourcesList />
<Footer
serverName={serverName}
serverVersion={serverVersion}
serverCommit={serverCommit}
serverBuildDate={serverBuildDate}
/>
</div>
);

Expand Down
33 changes: 33 additions & 0 deletions packages/playground/src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export function Footer({
serverName,
serverVersion,
serverCommit,
serverBuildDate,
}: {
serverName?: string;
serverVersion?: string;
serverCommit?: string;
serverBuildDate?: string;
}) {
const items = [
...(serverName ? [serverName] : []),
...(serverVersion ? [`v${serverVersion}`] : []),
...(serverCommit ? [`commit ${serverCommit}`] : []),
...(serverBuildDate ? [`built ${serverBuildDate}`] : []),
];

if (items.length === 0) {
return null;
}

return (
<footer className="pg-footer" aria-label="Server metadata">
{items.map((item, index) => (
<span key={item}>
{index > 0 && <span aria-hidden="true"> &#183; </span>}
{item}
</span>
))}
</footer>
);
}
16 changes: 16 additions & 0 deletions packages/playground/src/playground.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
--ring: oklch(0.705 0.015 286.067);
}

* {
box-sizing: border-box;
}

body {
margin: 0;
background: var(--background);
Expand All @@ -25,6 +29,7 @@ body {
max-width: 640px;
margin: 0 auto;
padding: 48px 24px 32px;
min-height: 100vh;
display: flex;
flex-direction: column;
gap: 40px;
Expand Down Expand Up @@ -52,6 +57,17 @@ body {
margin: 0;
}

.pg-footer {
margin-top: auto;
padding-top: 8px;
border-top: 1px solid var(--border);
font-size: 12px;
font-weight: 400;
color: var(--muted-foreground);
line-height: 1.5;
word-break: break-word;
}

/* ─── Server URL ─── */

.pg-url-block {
Expand Down
6 changes: 6 additions & 0 deletions packages/playground/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,17 @@ app.use(
app.use(express.json());

const serverDisplayName = process.env.SERVER_NAME || "speakeasy-docs";
const serverDisplayVersion = process.env.SERVER_VERSION;
const serverDisplayCommit = process.env.GIT_COMMIT;
const serverDisplayBuildDate = process.env.BUILD_DATE;

app.get("/api/config", (_req, res) => {
res.json({
...(password ? { token: password } : {}),
serverName: serverDisplayName,
...(serverDisplayVersion ? { serverVersion: serverDisplayVersion } : {}),
...(serverDisplayCommit ? { serverCommit: serverDisplayCommit } : {}),
...(serverDisplayBuildDate ? { serverBuildDate: serverDisplayBuildDate } : {}),
chatEnabled,
});
});
Expand Down
6 changes: 6 additions & 0 deletions packages/playground/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ function apiPlugin(): Plugin {
configureServer(server) {
const password = process.env.PLAYGROUND_PASSWORD;
const serverName = process.env.SERVER_NAME || "speakeasy-docs";
const serverVersion = process.env.SERVER_VERSION;
const serverCommit = process.env.GIT_COMMIT;
const serverBuildDate = process.env.BUILD_DATE;
const chatEnabled = !!process.env.GRAM_API_KEY;

server.middlewares.use((req, res, next) => {
Expand All @@ -111,6 +114,9 @@ function apiPlugin(): Plugin {
JSON.stringify({
...(password ? { token: password } : {}),
serverName,
...(serverVersion ? { serverVersion } : {}),
...(serverCommit ? { serverCommit } : {}),
...(serverBuildDate ? { serverBuildDate } : {}),
chatEnabled,
}),
);
Expand Down
46 changes: 38 additions & 8 deletions packages/server/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Command } from "commander";
import { startStdioServer } from "./stdio.js";
import { startHttpServer } from "./http.js";
import { createDocsMcpServerFactory } from "./create.js";
import { BuildInfo } from "./types.js";

const require = createRequire(import.meta.url);
const SERVER_VERSION = readPackageVersion();
Expand All @@ -23,6 +24,8 @@ interface ServerCliOptions {
transport: "stdio" | "http";
port: number;
customToolsJson?: string;
gitCommit?: string;
buildDate?: string;
}

const program = new Command();
Expand All @@ -31,9 +34,17 @@ program
.name("docs-mcp-server")
.description("Run @speakeasy-api/docs-mcp-server")
.requiredOption("--index-dir <path>", "Directory containing chunks.json and metadata.json")
.option("--name <value>", "MCP server name", "@speakeasy-api/docs-mcp-server")
.option(
"--name <value>",
"MCP server name (env: SERVER_NAME)",
process.env["SERVER_NAME"] || "@speakeasy-api/docs-mcp-server",
)
.option("--tool-prefix <value>", "Tool name prefix (e.g. 'acme' produces acme_search_docs)")
.option("--version <value>", "MCP server version", SERVER_VERSION)
.option(
"--version <value>",
"MCP server version (env: SERVER_VERSION)",
process.env["SERVER_VERSION"] || SERVER_VERSION,
)
.option("--query-embedding-api-key <value>", "Query embedding API key (or set OPENAI_API_KEY)")
.option("--query-embedding-base-url <value>", "Query embedding API base URL")
.option("--query-embedding-batch-size <number>", "Query embedding batch size", parseIntOption)
Expand All @@ -51,7 +62,28 @@ program
"--custom-tools-json <json>",
"JSON array of custom tool definitions [{name, description, inputSchema}], each registered with an echo handler",
)
.option(
"--git-commit <value>",
"Git commit SHA to include in server info (env: GIT_COMMIT)",
process.env["GIT_COMMIT"],
)
.option(
"--build-date <value>",
"Build date to include in server info (env: BUILD_DATE)",
process.env["BUILD_DATE"],
)
.action(async (options: ServerCliOptions) => {
const serverName =
options.name === "@speakeasy-api/docs-mcp-server" && options.toolPrefix
? `${options.toolPrefix}-docs-server`
: options.name;
const buildInfo: BuildInfo = {
name: serverName,
version: options.version,
gitCommit: options.gitCommit,
buildDate: options.buildDate,
};

const customTools = options.customToolsJson
? (
JSON.parse(options.customToolsJson) as Array<{
Expand All @@ -68,11 +100,6 @@ program
}))
: [];

const serverName =
options.name === "@speakeasy-api/docs-mcp-server" && options.toolPrefix
? `${options.toolPrefix}-docs-server`
: options.name;

const mcpServerFactory = await createDocsMcpServerFactory({
serverName,
serverVersion: options.version,
Expand All @@ -88,7 +115,10 @@ program
});

if (options.transport === "http") {
await startHttpServer(mcpServerFactory, { port: options.port });
await startHttpServer(mcpServerFactory, {
buildInfo,
port: options.port,
});
} else {
await startStdioServer(mcpServerFactory);
}
Expand Down
25 changes: 23 additions & 2 deletions packages/server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import http from "node:http";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import type { AuthInfo } from "./types.js";
import type { AuthInfo, BuildInfo } from "./types.js";

export interface StartHttpServerOptions {
buildInfo: BuildInfo;
port?: number;
/**
* Async hook called before each request is processed.
Expand Down Expand Up @@ -81,7 +82,7 @@ function createStatefulTransport(

export async function startHttpServer(
factory: () => McpServer,
options: StartHttpServerOptions = {},
options: StartHttpServerOptions,
): Promise<HttpServerHandle> {
const port = options.port ?? 20310;
const sessionManager = new SessionManager();
Expand Down Expand Up @@ -116,10 +117,12 @@ export async function startHttpServer(
return { httpServer, port: actualPort };
}

const DOCS_MCP_HEADER = "DOCS-MCP";
const CORS_HEADERS: Record<string, string> = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "*",
"Access-Control-Expose-Headers": [DOCS_MCP_HEADER].join(", "),
"Access-Control-Max-Age": "86400",
};

Expand All @@ -137,6 +140,14 @@ async function handleRequest(
sessionManager: SessionManager,
): Promise<void> {
const url = new URL(req.url ?? "/", "http://localhost");
const { buildInfo } = options;
res.setHeader(DOCS_MCP_HEADER, makeBuildInfoHeader(buildInfo));

if (req.method === "GET" && url.pathname === "/healthz") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ build: buildInfo }, null, 2));
return;
}

if (url.pathname !== "/mcp") {
res.writeHead(405, { "Content-Type": "text/plain" });
Expand Down Expand Up @@ -334,3 +345,13 @@ function readBody(req: http.IncomingMessage): Promise<string> {
function getHeaderValue(header: string | string[] | undefined): string | undefined {
return typeof header === "string" ? header : undefined;
}

function makeBuildInfoHeader(buildInfo: BuildInfo): string {
const arr: string[] = [];
arr.push(`name=${buildInfo.name}`);
arr.push(`version=${buildInfo.version}`);
if (buildInfo.gitCommit) arr.push(`git=${buildInfo.gitCommit}`);
if (buildInfo.buildDate) arr.push(`date=${buildInfo.buildDate}`);

return arr.join(" ");
}
7 changes: 7 additions & 0 deletions packages/server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ export type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
import type { CallToolResult, ListToolsResult } from "@modelcontextprotocol/sdk/types.js";
import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";

export interface BuildInfo {
name: string;
version: string;
gitCommit?: string | undefined;
buildDate?: string | undefined;
}

export interface ToolCallContext {
/** Validated auth info from transport middleware (HTTP only). */
authInfo?: AuthInfo;
Expand Down
Loading