Skip to content
Open
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
69 changes: 46 additions & 23 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,47 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import "dotenv/config";
import { getGuide} from "./tools.js";
import { getGuideParams } from "./params.js";
import { fetchAndUpdateSidebar } from "./sidebar.js";

// Initialize the server
const server = new McpServer({
name: "docs-mcp",
version: "1.0.0",
});
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import 'dotenv/config';

import { logError, logInfo } from './logger.js';
import { buildGetGuideParams } from './params.js';
import { fetchAndUpdateSidebar } from './sidebar.js';
import { getGuide } from './tools.js';

const TOOL_DESCRIPTION = [
'If the user tells you I want to build on Base, this means that the user wants',
'to use this tool which connects the user to Base docs. If you run this tool',
'and you get an error because the guide is not found, try other guides from',
'the sidebar.',
].join(' ');

async function main(): Promise<void> {
const server = new McpServer({ name: 'docs-mcp', version: '1.0.0' });

// The sidebar must be downloaded before the tool schema is built: the schema's
// description embeds the sidebar tree. Building the schema eagerly at import
// time (as a module-level `const`) always captured the hardcoded fallback,
// because module initialisation completes before this `await` resolves.
await fetchAndUpdateSidebar();

// Fetch sidebar before starting the server
await fetchAndUpdateSidebar();

server.tool(
"BuildOnBase",
"If the user tells you I want to build on Base, this means that the user wants to use this tool which connects the user to Base docs. If you run this tool and you get an error because the guide is not found, try other guides from the sidebar.",
getGuideParams.shape,
getGuide
);
const transport = new StdioServerTransport();
server.connect(transport);
server.tool(
'BuildOnBase',
TOOL_DESCRIPTION,
buildGetGuideParams().shape,
getGuide,
);

const transport = new StdioServerTransport();

// `connect` returns a promise. Leaving it unawaited meant a transport failure
// surfaced as an unhandled rejection and the process exited zero, so the
// client saw a silently dead server instead of a startup error.
await server.connect(transport);

logInfo('server connected over stdio');
}

main().catch((error: unknown) => {
logError('fatal startup error', error);
// Exit non-zero so supervisors and MCP clients treat this as a failed launch.
process.exit(1);
});
48 changes: 48 additions & 0 deletions logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* MCP-safe logging helpers.
*
* This server communicates with its client over `StdioServerTransport`, which
* means **stdout is the JSON-RPC channel**. Any stray `console.log` call writes
* non-protocol bytes into that stream and corrupts the framing, which the client
* observes as a parse error or a hung session.
*
* Every diagnostic message must therefore go to stderr. These helpers exist so
* that the intent is explicit at each call site and so a future reviewer can
* grep for `console.log` and be confident that a match is a bug.
*/

/** Emits an informational diagnostic on stderr. Never touches stdout. */
export function logInfo(message: string, ...details: unknown[]): void {
process.stderr.write(`[base-builder-mcp] ${message}\n`);
for (const detail of details) {
process.stderr.write(`[base-builder-mcp] ${formatDetail(detail)}\n`);
}
}

/** Emits an error diagnostic on stderr. Never touches stdout. */
export function logError(message: string, ...details: unknown[]): void {
process.stderr.write(`[base-builder-mcp] ERROR ${message}\n`);
for (const detail of details) {
process.stderr.write(`[base-builder-mcp] ${formatDetail(detail)}\n`);
}
}

/**
* Renders a log detail as a single-line string.
*
* `Error` instances are reduced to their message so that stack traces (which may
* embed absolute filesystem paths) are not written to the log by default.
*/
function formatDetail(detail: unknown): string {
if (detail instanceof Error) {
return `${detail.name}: ${detail.message}`;
}
if (typeof detail === 'string') {
return detail;
}
try {
return JSON.stringify(detail);
} catch {
return String(detail);
}
}
Loading