Marginalia MCP Server
by bmorphism
- src
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { loadConfig } from "./config.js";
import { HoneycombAPI } from "./api/client.js";
import { z } from "zod";
import {
DatasetArgumentsSchema,
QueryToolSchema,
ColumnAnalysisSchema,
} from "./types/schema.js";
import { Dataset } from "./types/api.js";
import { HoneycombError } from "./utils/errors.js";
import process from "node:process";
async function handleToolError(
error: unknown,
toolName: string,
): Promise<{ content: { type: "text"; text: string }[] }> {
let errorMessage = "Unknown error occurred";
if (error instanceof HoneycombError) {
errorMessage = `Honeycomb API error (${error.statusCode}): ${error.message}`;
} else if (error instanceof Error) {
errorMessage = error.message;
}
// Log the error to stderr for debugging
console.error(`Tool '${toolName}' failed:`, error);
return {
content: [
{
type: "text",
text: `Failed to execute tool '${toolName}': ${errorMessage}\n\n` +
`Please verify:\n` +
`- The environment name is correct and configured in .mcp-honeycomb.json\n` +
`- Your API key is valid\n` +
`- The dataset exists (if specified)\n` +
`- Required parameters are provided correctly`,
},
],
};
}
// Create a main async function to run everything
async function main() {
// Load config and create API client
const config = loadConfig();
const api = new HoneycombAPI(config);
// Create server with proper initialization options
const server = new McpServer({
name: "honeycomb",
version: "1.0.0"
});
// Register resource for datasets
server.resource(
"datasets",
new ResourceTemplate("honeycomb://{environment}/{dataset}", {
list: async () => {
const environments = api.getEnvironments();
const resources: { uri: string; name: string; description?: string }[] = [];
for (const env of environments) {
try {
const datasets = await api.listDatasets(env);
datasets.forEach((dataset: Dataset) => {
resources.push({
uri: `honeycomb://${env}/${dataset.slug}`,
name: dataset.name,
description: dataset.description || `Dataset ${dataset.name} in environment ${env}`,
});
});
} catch (error) {
console.error(`Error listing datasets for environment ${env}:`, error);
}
}
return { resources };
}
}),
async (uri, { environment, dataset }) => {
try {
if (dataset) {
// Get specific dataset
const datasetInfo = await api.getDataset(environment as string, dataset as string);
const columns = await api.getVisibleColumns(environment as string, dataset as string);
const datasetWithColumns = {
name: datasetInfo.name,
columns: columns.map((c) => ({
name: c.key_name,
type: c.type,
description: c.description,
})),
};
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(datasetWithColumns, null, 2),
},
],
};
} else {
// List all datasets
const datasets = await api.listDatasets(environment as string);
return {
contents: datasets.map((dataset: Dataset) => ({
uri: `honeycomb://${environment}/${dataset.slug}`,
text: JSON.stringify({
name: dataset.name,
description: dataset.description,
}, null, 2),
})),
};
}
} catch (error) {
throw new Error(`Failed to read dataset: ${error}`);
}
}
);
// Register tools
server.tool(
"list_datasets",
{ environment: z.string() },
async ({ environment }) => {
try {
const datasets = await api.listDatasets(environment);
return {
content: [
{
type: "text",
text: JSON.stringify(datasets, null, 2),
},
],
};
} catch (error) {
return handleToolError(error, "list_datasets");
}
}
);
server.tool(
"get_columns",
{
environment: z.string(),
dataset: z.string(),
},
async ({ environment, dataset }) => {
try {
const columns = await api.getVisibleColumns(environment, dataset);
return {
content: [
{
type: "text",
text: JSON.stringify(columns, null, 2),
},
],
};
} catch (error) {
return handleToolError(error, "get_columns");
}
}
);
server.tool(
"run_query",
QueryToolSchema.shape,
async (params) => {
try {
const result = await api.runAnalysisQuery(params.environment, params.dataset, params);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return handleToolError(error, "run_query");
}
}
);
server.tool(
"analyze_column",
ColumnAnalysisSchema.shape,
async (params) => {
try {
const result = await api.analyzeColumn(params.environment, params.dataset, params);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return handleToolError(error, "analyze_column");
}
}
);
server.tool(
"list_slos",
DatasetArgumentsSchema.shape,
async ({ environment, dataset }) => {
try {
const slos = await api.getSLOs(environment, dataset);
return {
content: [
{
type: "text",
text: JSON.stringify(slos, null, 2),
},
],
};
} catch (error) {
return handleToolError(error, "list_slos");
}
}
);
server.tool(
"get_slo",
{
environment: z.string(),
dataset: z.string(),
sloId: z.string(),
},
async ({ environment, dataset, sloId }) => {
try {
const slo = await api.getSLO(environment, dataset, sloId);
return {
content: [
{
type: "text",
text: JSON.stringify(slo, null, 2),
},
],
};
} catch (error) {
return handleToolError(error, "get_slo");
}
}
);
server.tool(
"list_triggers",
DatasetArgumentsSchema.shape,
async ({ environment, dataset }) => {
try {
const triggers = await api.getTriggers(environment, dataset);
return {
content: [
{
type: "text",
text: JSON.stringify(triggers, null, 2),
},
],
};
} catch (error) {
return handleToolError(error, "list_triggers");
}
}
);
server.tool(
"get_trigger",
{
environment: z.string(),
dataset: z.string(),
triggerId: z.string(),
},
async ({ environment, dataset, triggerId }) => {
try {
const trigger = await api.getTrigger(environment, dataset, triggerId);
return {
content: [
{
type: "text",
text: JSON.stringify(trigger, null, 2),
},
],
};
} catch (error) {
return handleToolError(error, "get_trigger");
}
}
);
// Create transport and start server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Honeycomb MCP Server running on stdio");
}
// Run main with proper error handling
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
}