#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import {
loadCsv,
describeCsv,
filterRows,
aggregateColumn,
groupBy,
sortRows,
} from "./csv.js";
const server = new McpServer({
name: "mcp-csv-analyst",
version: "1.0.0",
});
// Tool: Load and describe a CSV file
server.tool(
"csv_describe",
"Load a CSV file and return its schema, row count, and column statistics",
{
file_path: z.string().describe("Absolute path to the CSV file"),
},
async ({ file_path }) => {
const data = loadCsv(file_path);
const description = describeCsv(data);
return {
content: [{ type: "text" as const, text: description }],
};
},
);
// Tool: Query/filter CSV rows
server.tool(
"csv_filter",
"Filter CSV rows by a column condition. Returns matching rows as JSON.",
{
file_path: z.string().describe("Absolute path to the CSV file"),
column: z.string().describe("Column name to filter on"),
operator: z
.enum(["eq", "neq", "gt", "gte", "lt", "lte", "contains", "starts_with"])
.describe("Comparison operator"),
value: z.string().describe("Value to compare against"),
limit: z
.number()
.optional()
.default(50)
.describe("Max rows to return (default 50)"),
sort_by: z.string().optional().describe("Column to sort results by"),
sort_dir: z
.enum(["asc", "desc"])
.optional()
.default("asc")
.describe("Sort direction"),
},
async ({ file_path, column, operator, value, limit, sort_by, sort_dir }) => {
const data = loadCsv(file_path);
let results = filterRows(data, column, operator, value);
if (sort_by) {
results = sortRows(results, sort_by, sort_dir);
}
const limited = results.slice(0, limit);
const text = [
`Found ${results.length} matching rows (showing ${limited.length}):`,
JSON.stringify(limited, null, 2),
].join("\n");
return { content: [{ type: "text" as const, text }] };
},
);
// Tool: Aggregate a column
server.tool(
"csv_aggregate",
"Compute an aggregate (sum, avg, min, max, count, median) on a numeric column",
{
file_path: z.string().describe("Absolute path to the CSV file"),
column: z.string().describe("Numeric column to aggregate"),
operation: z
.enum(["sum", "avg", "min", "max", "count", "median"])
.describe("Aggregation function"),
},
async ({ file_path, column, operation }) => {
const data = loadCsv(file_path);
const result = aggregateColumn(data, column, operation);
return {
content: [
{
type: "text" as const,
text: `${operation}(${column}) = ${result}`,
},
],
};
},
);
// Tool: Group by + aggregate
server.tool(
"csv_group_by",
"Group rows by a column and compute an aggregate on another column",
{
file_path: z.string().describe("Absolute path to the CSV file"),
group_column: z.string().describe("Column to group by"),
agg_column: z.string().describe("Numeric column to aggregate"),
operation: z
.enum(["sum", "avg", "count", "min", "max"])
.describe("Aggregation function"),
},
async ({ file_path, group_column, agg_column, operation }) => {
const data = loadCsv(file_path);
const result = groupBy(data, group_column, agg_column, operation);
const text = [
`Group by ${group_column}, ${operation}(${agg_column}):`,
JSON.stringify(result, null, 2),
].join("\n");
return { content: [{ type: "text" as const, text }] };
},
);
// Tool: Get sample rows
server.tool(
"csv_sample",
"Get a sample of rows from a CSV file",
{
file_path: z.string().describe("Absolute path to the CSV file"),
count: z
.number()
.optional()
.default(10)
.describe("Number of sample rows (default 10)"),
offset: z
.number()
.optional()
.default(0)
.describe("Row offset to start from"),
},
async ({ file_path, count, offset }) => {
const data = loadCsv(file_path);
const sample = data.rows.slice(offset, offset + count);
const text = [
`Rows ${offset + 1}-${offset + sample.length} of ${data.rowCount}:`,
JSON.stringify(sample, null, 2),
].join("\n");
return { content: [{ type: "text" as const, text }] };
},
);
// Tool: Get unique values in a column
server.tool(
"csv_unique",
"Get unique values in a column with their counts",
{
file_path: z.string().describe("Absolute path to the CSV file"),
column: z.string().describe("Column name"),
limit: z
.number()
.optional()
.default(50)
.describe("Max unique values to return"),
},
async ({ file_path, column, limit }) => {
const data = loadCsv(file_path);
const counts: Record<string, number> = {};
for (const row of data.rows) {
const val = row[column] ?? "(empty)";
counts[val] = (counts[val] || 0) + 1;
}
const sorted = Object.entries(counts)
.sort((a, b) => b[1] - a[1])
.slice(0, limit);
const text = [
`Unique values in "${column}" (${Object.keys(counts).length} total):`,
...sorted.map(([val, count]) => ` ${val}: ${count}`),
].join("\n");
return { content: [{ type: "text" as const, text }] };
},
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);