#!/usr/bin/env bun
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as crypto from "crypto";
import { evaluate, unit, mean, median, mode, std, min, max, sum } from "mathjs";
const server = new McpServer({
name: "basic-tools",
version: "1.0.0",
});
// ============================================================================
// TIME TOOLS
// ============================================================================
server.tool(
"now",
"Get current date and time. Use this whenever you need to know the current time.",
{
timezone: z.string().optional().describe("IANA timezone (e.g., 'America/New_York'). Defaults to UTC."),
},
async ({ timezone }) => {
const tz = timezone || "UTC";
const now = new Date();
const formatted = now.toLocaleString("en-US", {
timeZone: tz,
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short",
});
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
iso: now.toISOString(),
unix: Math.floor(now.getTime() / 1000),
unix_ms: now.getTime(),
formatted,
timezone: tz,
}, null, 2),
},
],
};
}
);
server.tool(
"date_diff",
"Calculate the difference between two dates. Use this for date arithmetic.",
{
start: z.string().describe("Start date (ISO 8601 or common format)"),
end: z.string().describe("End date (ISO 8601 or common format)"),
unit: z.enum(["seconds", "minutes", "hours", "days", "weeks", "months", "years"]).default("days"),
},
async ({ start, end, unit: unitType }) => {
const startDate = new Date(start);
const endDate = new Date(end);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return {
content: [{ type: "text" as const, text: "Error: Invalid date format" }],
isError: true,
};
}
const diffMs = endDate.getTime() - startDate.getTime();
const divisors: Record<string, number> = {
seconds: 1000,
minutes: 1000 * 60,
hours: 1000 * 60 * 60,
days: 1000 * 60 * 60 * 24,
weeks: 1000 * 60 * 60 * 24 * 7,
months: 1000 * 60 * 60 * 24 * 30.44, // average month
years: 1000 * 60 * 60 * 24 * 365.25,
};
const diff = diffMs / divisors[unitType]!;
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
start: startDate.toISOString(),
end: endDate.toISOString(),
difference: diff,
unit: unitType,
exact: Number.isInteger(diff) ? diff : diff.toFixed(4),
}, null, 2),
},
],
};
}
);
server.tool(
"date_add",
"Add or subtract time from a date. Use negative amounts to subtract.",
{
date: z.string().describe("Starting date (ISO 8601 or common format). Use 'now' for current time."),
amount: z.number().describe("Amount to add (negative to subtract)"),
unit: z.enum(["seconds", "minutes", "hours", "days", "weeks", "months", "years"]),
},
async ({ date, amount, unit: unitType }) => {
const startDate = date.toLowerCase() === "now" ? new Date() : new Date(date);
if (isNaN(startDate.getTime())) {
return {
content: [{ type: "text" as const, text: "Error: Invalid date format" }],
isError: true,
};
}
const result = new Date(startDate);
switch (unitType) {
case "seconds":
result.setSeconds(result.getSeconds() + amount);
break;
case "minutes":
result.setMinutes(result.getMinutes() + amount);
break;
case "hours":
result.setHours(result.getHours() + amount);
break;
case "days":
result.setDate(result.getDate() + amount);
break;
case "weeks":
result.setDate(result.getDate() + amount * 7);
break;
case "months":
result.setMonth(result.getMonth() + amount);
break;
case "years":
result.setFullYear(result.getFullYear() + amount);
break;
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
original: startDate.toISOString(),
result: result.toISOString(),
operation: `${amount >= 0 ? "+" : ""}${amount} ${unitType}`,
day_of_week: result.toLocaleDateString("en-US", { weekday: "long" }),
}, null, 2),
},
],
};
}
);
server.tool(
"unix_timestamp",
"Convert between Unix timestamps and human-readable dates.",
{
value: z.union([z.string(), z.number()]).describe("Unix timestamp (number) or date string to convert"),
direction: z.enum(["to_unix", "from_unix"]).describe("Conversion direction"),
},
async ({ value, direction }) => {
if (direction === "to_unix") {
const date = new Date(value);
if (isNaN(date.getTime())) {
return {
content: [{ type: "text" as const, text: "Error: Invalid date format" }],
isError: true,
};
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
input: value,
unix: Math.floor(date.getTime() / 1000),
unix_ms: date.getTime(),
iso: date.toISOString(),
}, null, 2),
},
],
};
} else {
const timestamp = typeof value === "string" ? parseInt(value, 10) : value;
// Handle both seconds and milliseconds
const ms = timestamp > 9999999999 ? timestamp : timestamp * 1000;
const date = new Date(ms);
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
input: timestamp,
iso: date.toISOString(),
formatted: date.toLocaleString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short",
}),
}, null, 2),
},
],
};
}
}
);
// ============================================================================
// MATH TOOLS
// ============================================================================
server.tool(
"calculate",
"Evaluate mathematical expressions. Supports standard math operations, functions, and constants. Use this for ANY math calculation.",
{
expression: z.string().describe("Math expression (e.g., '2 + 2', 'sqrt(16)', 'sin(pi/2)', '15% of 200')"),
},
async ({ expression }) => {
try {
// Handle percentage phrases like "15% of 200"
const percentOfRegex = /(\d+(?:\.\d+)?)\s*%\s*of\s*(\d+(?:\.\d+)?)/gi;
let expr = expression.replace(percentOfRegex, (_, pct, num) => `(${pct}/100)*${num}`);
const result = evaluate(expr);
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
expression,
result: typeof result === "number" ? result : result.toString(),
type: typeof result,
}, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: "text" as const, text: `Error: ${error instanceof Error ? error.message : "Invalid expression"}` }],
isError: true,
};
}
}
);
server.tool(
"convert_units",
"Convert between units of measurement (length, weight, temperature, etc.).",
{
value: z.number().describe("The value to convert"),
from: z.string().describe("Source unit (e.g., 'km', 'lb', 'celsius', 'USD')"),
to: z.string().describe("Target unit (e.g., 'mile', 'kg', 'fahrenheit', 'EUR')"),
},
async ({ value, from, to }) => {
try {
const result = unit(value, from).to(to);
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
input: `${value} ${from}`,
result: result.toNumber(),
formatted: result.toString(),
}, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: "text" as const, text: `Error: ${error instanceof Error ? error.message : "Conversion failed"}` }],
isError: true,
};
}
}
);
server.tool(
"statistics",
"Calculate statistical measures for a set of numbers.",
{
numbers: z.array(z.number()).describe("Array of numbers to analyze"),
},
async ({ numbers }) => {
if (numbers.length === 0) {
return {
content: [{ type: "text" as const, text: "Error: Empty array" }],
isError: true,
};
}
const sorted = [...numbers].sort((a, b) => a - b);
const modeResult = mode(numbers);
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
count: numbers.length,
sum: sum(numbers),
mean: mean(numbers),
median: median(numbers),
mode: Array.isArray(modeResult) ? modeResult : [modeResult],
min: min(numbers),
max: max(numbers),
range: max(numbers) - min(numbers),
std_dev: std(numbers),
sorted,
}, null, 2),
},
],
};
}
);
// ============================================================================
// STRING / COUNTING TOOLS
// ============================================================================
server.tool(
"count",
"Count characters, words, lines, and bytes in text. Use this instead of trying to count manually.",
{
text: z.string().describe("Text to analyze"),
},
async ({ text }) => {
const chars = [...text].length; // Handles Unicode correctly
const bytes = Buffer.byteLength(text, "utf8");
const words = text.trim() ? text.trim().split(/\s+/).length : 0;
const lines = text.split("\n").length;
const sentences = text.split(/[.!?]+/).filter(s => s.trim()).length;
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
characters: chars,
characters_no_spaces: [...text.replace(/\s/g, "")].length,
words,
lines,
sentences,
bytes,
paragraphs: text.split(/\n\s*\n/).filter(p => p.trim()).length,
}, null, 2),
},
],
};
}
);
server.tool(
"hash",
"Generate cryptographic hash of text.",
{
text: z.string().describe("Text to hash"),
algorithm: z.enum(["md5", "sha1", "sha256", "sha512"]).default("sha256"),
},
async ({ text, algorithm }) => {
const hash = crypto.createHash(algorithm).update(text).digest("hex");
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
algorithm,
hash,
input_length: text.length,
}, null, 2),
},
],
};
}
);
server.tool(
"uuid",
"Generate a new UUID (v4).",
{},
async () => {
const uuid = crypto.randomUUID();
return {
content: [
{
type: "text" as const,
text: JSON.stringify({ uuid }, null, 2),
},
],
};
}
);
server.tool(
"base64",
"Encode or decode base64 strings.",
{
text: z.string().describe("Text to encode or decode"),
direction: z.enum(["encode", "decode"]),
},
async ({ text, direction }) => {
try {
const result = direction === "encode"
? Buffer.from(text).toString("base64")
: Buffer.from(text, "base64").toString("utf8");
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
input: text,
output: result,
direction,
}, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: "text" as const, text: `Error: ${error instanceof Error ? error.message : "Encoding/decoding failed"}` }],
isError: true,
};
}
}
);
server.tool(
"regex_test",
"Test if a string matches a regular expression pattern.",
{
text: z.string().describe("Text to test"),
pattern: z.string().describe("Regular expression pattern"),
flags: z.string().optional().describe("Regex flags (e.g., 'gi' for global, case-insensitive)"),
},
async ({ text, pattern, flags }) => {
try {
const regex = new RegExp(pattern, flags);
const matches = text.match(regex);
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
pattern,
flags: flags || "",
matches: matches !== null,
match_count: matches?.length || 0,
matched_values: matches || [],
}, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: "text" as const, text: `Error: ${error instanceof Error ? error.message : "Invalid regex"}` }],
isError: true,
};
}
}
);
// ============================================================================
// RANDOM TOOLS
// ============================================================================
server.tool(
"random_int",
"Generate a cryptographically secure random integer within a range.",
{
min: z.number().int().describe("Minimum value (inclusive)"),
max: z.number().int().describe("Maximum value (inclusive)"),
},
async ({ min: minVal, max: maxVal }) => {
if (minVal > maxVal) {
return {
content: [{ type: "text" as const, text: "Error: min must be <= max" }],
isError: true,
};
}
const range = maxVal - minVal + 1;
const randomBytes = crypto.randomBytes(4);
const randomValue = randomBytes.readUInt32BE(0);
const result = minVal + (randomValue % range);
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
min: minVal,
max: maxVal,
result,
}, null, 2),
},
],
};
}
);
server.tool(
"random_float",
"Generate a cryptographically secure random float between 0 and 1 (or custom range).",
{
min: z.number().default(0).describe("Minimum value"),
max: z.number().default(1).describe("Maximum value"),
precision: z.number().int().min(1).max(15).default(6).describe("Decimal places"),
},
async ({ min: minVal, max: maxVal, precision }) => {
const randomBytes = crypto.randomBytes(8);
const randomValue = randomBytes.readBigUInt64BE(0);
const normalized = Number(randomValue) / Number(BigInt(2) ** BigInt(64));
const scaled = minVal + normalized * (maxVal - minVal);
const result = Number(scaled.toFixed(precision));
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
min: minVal,
max: maxVal,
result,
}, null, 2),
},
],
};
}
);
server.tool(
"random_choice",
"Select a random item from a list.",
{
items: z.array(z.any()).describe("Array of items to choose from"),
count: z.number().int().min(1).default(1).describe("Number of items to select"),
},
async ({ items, count }) => {
if (items.length === 0) {
return {
content: [{ type: "text" as const, text: "Error: Empty array" }],
isError: true,
};
}
if (count > items.length) {
return {
content: [{ type: "text" as const, text: "Error: count exceeds array length" }],
isError: true,
};
}
const selected: unknown[] = [];
const remaining = [...items];
for (let i = 0; i < count; i++) {
const randomBytes = crypto.randomBytes(4);
const randomValue = randomBytes.readUInt32BE(0);
const index = randomValue % remaining.length;
selected.push(remaining.splice(index, 1)[0]);
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
from: items,
selected: count === 1 ? selected[0] : selected,
}, null, 2),
},
],
};
}
);
server.tool(
"shuffle",
"Randomly shuffle an array (Fisher-Yates algorithm).",
{
items: z.array(z.any()).describe("Array to shuffle"),
},
async ({ items }) => {
const shuffled = [...items];
for (let i = shuffled.length - 1; i > 0; i--) {
const randomBytes = crypto.randomBytes(4);
const randomValue = randomBytes.readUInt32BE(0);
const j = randomValue % (i + 1);
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
original: items,
shuffled,
}, null, 2),
},
],
};
}
);
// ============================================================================
// VALIDATION TOOLS
// ============================================================================
server.tool(
"validate",
"Validate strings against common formats (email, URL, IP, UUID, JSON).",
{
value: z.string().describe("Value to validate"),
type: z.enum(["email", "url", "ipv4", "ipv6", "uuid", "json", "credit_card"]),
},
async ({ value, type }) => {
const patterns: Record<string, RegExp> = {
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
ipv6: /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::$|^([0-9a-fA-F]{1,4}:){1,7}:$|^:([0-9a-fA-F]{1,4}:){1,7}$|^([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$/,
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
};
let valid = false;
let error: string | undefined;
let details: Record<string, unknown> = {};
switch (type) {
case "email":
case "ipv4":
case "ipv6":
case "uuid":
valid = patterns[type]!.test(value);
if (!valid) error = `Invalid ${type} format`;
break;
case "url":
try {
const url = new URL(value);
valid = true;
details = {
protocol: url.protocol,
hostname: url.hostname,
pathname: url.pathname,
search: url.search,
};
} catch {
valid = false;
error = "Invalid URL format";
}
break;
case "json":
try {
const parsed = JSON.parse(value);
valid = true;
details = {
type: Array.isArray(parsed) ? "array" : typeof parsed,
keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : undefined,
};
} catch (e) {
valid = false;
error = e instanceof Error ? e.message : "Invalid JSON";
}
break;
case "credit_card":
// Luhn algorithm
const digits = value.replace(/\D/g, "");
if (digits.length < 13 || digits.length > 19) {
valid = false;
error = "Invalid card number length";
} else {
let sum = 0;
let isEven = false;
for (let i = digits.length - 1; i >= 0; i--) {
let digit = parseInt(digits[i]!, 10);
if (isEven) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
isEven = !isEven;
}
valid = sum % 10 === 0;
if (!valid) error = "Failed Luhn check";
}
break;
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
value,
type,
valid,
error,
...details,
}, null, 2),
},
],
};
}
);
// ============================================================================
// JSON TOOLS
// ============================================================================
server.tool(
"json_parse",
"Parse and format JSON. Use this to validate JSON or extract specific fields.",
{
json: z.string().describe("JSON string to parse"),
path: z.string().optional().describe("Optional JSON path to extract (dot notation, e.g., 'user.name')"),
},
async ({ json, path }) => {
try {
const parsed = JSON.parse(json);
if (path) {
const parts = path.split(".");
let value: unknown = parsed;
for (const part of parts) {
if (value === null || value === undefined) break;
value = (value as Record<string, unknown>)[part];
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
path,
value,
type: typeof value,
}, null, 2),
},
],
};
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
valid: true,
type: Array.isArray(parsed) ? "array" : typeof parsed,
formatted: JSON.stringify(parsed, null, 2),
}, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: "text" as const, text: `Error: ${error instanceof Error ? error.message : "Invalid JSON"}` }],
isError: true,
};
}
}
);
// ============================================================================
// NETWORK/DNS TOOLS (CLI Wrappers)
// ============================================================================
server.tool(
"dns_lookup",
"Perform DNS lookup for a hostname.",
{
hostname: z.string().describe("Hostname to lookup"),
type: z.enum(["A", "AAAA", "MX", "TXT", "NS", "CNAME"]).default("A").describe("Record type"),
},
async ({ hostname, type }) => {
try {
const proc = Bun.spawn(["dig", "+short", hostname, type], {
stdout: "pipe",
stderr: "pipe",
});
const output = await new Response(proc.stdout).text();
const records = output.trim().split("\n").filter(Boolean);
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
hostname,
type,
records,
}, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: "text" as const, text: `Error: ${error instanceof Error ? error.message : "DNS lookup failed"}` }],
isError: true,
};
}
}
);
server.tool(
"url_parse",
"Parse a URL into its components.",
{
url: z.string().describe("URL to parse"),
},
async ({ url }) => {
try {
const parsed = new URL(url);
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
href: parsed.href,
protocol: parsed.protocol,
hostname: parsed.hostname,
port: parsed.port || "default",
pathname: parsed.pathname,
search: parsed.search,
hash: parsed.hash,
searchParams: Object.fromEntries(parsed.searchParams),
}, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: "text" as const, text: `Error: ${error instanceof Error ? error.message : "Invalid URL"}` }],
isError: true,
};
}
}
);
// ============================================================================
// START SERVER
// ============================================================================
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("basic-tools MCP server running on stdio");
}
main().catch(console.error);