Obsidian MCP Server
- src
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
GetPromptRequestSchema,
ListPromptsRequestSchema,
ListToolsRequestSchema,
TextContent,
ListRootsRequestSchema,
Resource,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fetch from "node-fetch";
const server = new Server(
{ name: "macrostrat", version: "1.0.0" },
{
capabilities: {
tools: {},
prompts: {},
roots: {},
resources: {},
},
},
);
const API_SCHEMAS: Record<string, any> = {};
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources: Resource[] = [];
return { resources };
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const schema = API_SCHEMAS[request.params.uri];
if (!schema) throw new Error(`Unknown schema: ${request.params.uri}`);
return {
contents: [],
};
});
const PROMPTS = {
// find earthquakes
};
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {};
});
// Add type for valid prompt names
type PromptName = keyof typeof PROMPTS;
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const prompt = PROMPTS[request.params.name as PromptName];
if (!prompt) {
throw new Error(`Prompt not found: ${request.params.name}`);
}
return {};
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "find-earthquakes",
description:
"Query the USGS earthquake API to find earthquakes based on a variety of parameters",
inputSchema: {
type: "object",
properties: {
endTime: {
type: "string",
description:
"Limit to events on or before the specified end time. NOTE: All times use ISO8601 Date/Time format. Unless a timezone is specified, UTC is assumed. Defaults to NOW.",
},
startTime: {
type: "string",
description:
"Limit to events on or after the specified start time. NOTE: All times use ISO8601 Date/Time format. Unless a timezone is specified, UTC is assumed. Defaults to NOW - 30 days.",
},
minLatitude: {
type: "number",
description:
"Limit to events with a latitude larger than the specified minimum. NOTE: min values must be less than max values. Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
maxLatitude: {
type: "number",
description:
"Limit to events with a latitude smaller than the specified maximum. NOTE: min values must be less than max values. Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
minLongitude: {
type: "number",
description:
"Limit to events with a longitude larger than the specified minimum. NOTE: rectangles may cross the date line by using a minlongitude < -180 or maxlongitude > 180. NOTE: min values must be less than max values. Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
maxLongitude: {
type: "number",
description:
"Limit to events with a longitude smaller than the specified maximum. NOTE: rectangles may cross the date line by using a minlongitude < -180 or maxlongitude > 180. NOTE: min values must be less than max values. Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
latitude: {
type: "number",
description:
"Specify the latitude to be used for a circle radius search. NOTE: Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
longitude: {
type: "number",
description:
"Specify the longitude to be used for a circle radius search. NOTE: Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
maxradiuskm: {
type: "number",
description:
"Limit to events within the specified maximum number of kilometers from the geographic point defined by the latitude and longitude parameters. NOTE: Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
limit: {
type: "number",
description:
"Limit the number of events returned. NOTE: The service limits queries to 20000, and any that exceed this limit will generate a HTTP response code “400 Bad Request”.",
default: 100,
},
maxdepth: {
type: "number",
description:
"Limit to events with depth less than the specified maximum.",
},
maxmagnitude: {
type: "number",
description:
"Limit to events with a magnitude smaller than the specified maximum.",
},
mindepth: {
type: "number",
description:
"Limit to events with depth more than the specified minimum.",
},
minmagnitude: {
type: "number",
description:
"Limit to events with a magnitude larger than the specified minimum.",
},
orderby: {
type: "string",
description:
"Sort the results by the specified field. NOTE: The default is to sort by time, with the most recent events first.",
enum: ["time", "time-asc", "magnitude", "magnitude-asc"],
},
// alertlevel: {
// type: "string",
// description:
// "Limit to events with a specific PAGER alert level. ",
// enum: ["green", "yellow", "orange", "red"],
// },
},
},
},
{
name: "find-earthquake-details",
description: "Get details about an earthquake",
inputSchema: {
type: "object",
properties: {
eventid: {
type: "string",
description: "The earthquake eventid",
},
},
},
},
{
name: "count-earthquakes",
description: "Count the number of earthquakes returned from the USGS that are found based on a variety of search parameters",
inputSchema: {
type: "object",
properties: {
endTime: {
type: "string",
description:
"Limit to events on or before the specified end time. NOTE: All times use ISO8601 Date/Time format. Unless a timezone is specified, UTC is assumed. Defaults to NOW.",
},
startTime: {
type: "string",
description:
"Limit to events on or after the specified start time. NOTE: All times use ISO8601 Date/Time format. Unless a timezone is specified, UTC is assumed. Defaults to NOW - 30 days.",
},
minLatitude: {
type: "number",
description:
"Limit to events with a latitude larger than the specified minimum. NOTE: min values must be less than max values. Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
maxLatitude: {
type: "number",
description:
"Limit to events with a latitude smaller than the specified maximum. NOTE: min values must be less than max values. Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
minLongitude: {
type: "number",
description:
"Limit to events with a longitude larger than the specified minimum. NOTE: rectangles may cross the date line by using a minlongitude < -180 or maxlongitude > 180. NOTE: min values must be less than max values. Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
maxLongitude: {
type: "number",
description:
"Limit to events with a longitude smaller than the specified maximum. NOTE: rectangles may cross the date line by using a minlongitude < -180 or maxlongitude > 180. NOTE: min values must be less than max values. Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
latitude: {
type: "number",
description:
"Specify the latitude to be used for a circle radius search. NOTE: Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
longitude: {
type: "number",
description:
"Specify the longitude to be used for a circle radius search. NOTE: Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
maxradiuskm: {
type: "number",
description:
"Limit to events within the specified maximum number of kilometers from the geographic point defined by the latitude and longitude parameters. NOTE: Requests that use both rectangle and circle will return the intersection, which may be empty, use with caution.",
},
limit: {
type: "number",
description:
"Limit the number of events returned. NOTE: The service limits queries to 20000, and any that exceed this limit will generate a HTTP response code “400 Bad Request”.",
default: 100,
},
maxdepth: {
type: "number",
description:
"Limit to events with depth less than the specified maximum.",
},
maxmagnitude: {
type: "number",
description:
"Limit to events with a magnitude smaller than the specified maximum.",
},
mindepth: {
type: "number",
description:
"Limit to events with depth more than the specified minimum.",
},
minmagnitude: {
type: "number",
description:
"Limit to events with a magnitude larger than the specified minimum.",
},
orderby: {
type: "string",
description:
"Sort the results by the specified field. NOTE: The default is to sort by time, with the most recent events first.",
enum: ["time", "time-asc", "magnitude", "magnitude-asc"],
},
// alertlevel: {
// type: "string",
// description:
// "Limit to events with a specific PAGER alert level. ",
// enum: ["green", "yellow", "orange", "red"],
// },
},
},
}
],
};
});
// Add interface for earthquake API parameters
interface EarthquakeParams {
endTime?: string;
startTime?: string;
minLatitude?: number;
maxLatitude?: number;
minLongitude?: number;
maxLongitude?: number;
latitude?: number;
longitude?: number;
maxradiuskm?: number;
limit?: number;
maxdepth?: number;
maxmagnitude?: number;
mindepth?: number;
minmagnitude?: number;
orderby?: "time" | "time-asc" | "magnitude" | "magnitude-asc";
// alertlevel?: "green" | "yellow" | "orange" | "red";
}
server.setRequestHandler(CallToolRequestSchema, async (request) => {
let data: any;
if (request.params.name === "find-earthquakes") {
const params = request.params.arguments as EarthquakeParams;
// Create a record of params, converting camelCase to snake_case where needed
const paramMap: Record<string, string | number | undefined> = {
endtime: params.endTime,
starttime: params.startTime,
minlatitude: params.minLatitude,
maxlatitude: params.maxLatitude,
minlongitude: params.minLongitude,
maxlongitude: params.maxLongitude,
latitude: params.latitude,
longitude: params.longitude,
maxradiuskm: params.maxradiuskm,
limit: params.limit,
maxdepth: params.maxdepth,
maxmagnitude: params.maxmagnitude,
mindepth: params.mindepth,
minmagnitude: params.minmagnitude,
orderby: params.orderby,
// alertlevel: params.alertlevel,
};
// Filter out undefined values and convert to URLSearchParams compatible format
const searchParams = new URLSearchParams(
Object.entries(paramMap)
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => [key, value!.toString()]),
);
const response = await fetch(
`https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&${searchParams}`,
);
if (!response.ok) {
throw new Error(
`Failed to fetch earthquakes: ${response.statusText} when using params:${searchParams.toString()}`,
);
}
data = await response.json();
// Process the features to clean up the data
data = data.features.map((feature: any) => ({
eventid: feature.id,
properties: {
magnitude: feature.properties.mag,
magType: feature.properties.magType,
location: feature.properties.place,
time: feature.properties.time,
updated: feature.properties.updated,
felt: feature.properties.felt,
type: feature.properties.type,
url: feature.properties.url,
tsunami: feature.properties.tsunami,
net: feature.properties.net,
status: feature.properties.net,
rms: feature.properties.rms,
// Only include if it exists
...(feature.properties.alert && { alert: feature.properties.alert }),
...(feature.properties.cdi && { cdi: feature.properties.cdi}),
...(feature.properties.mmi && { mmi: feature.properties.mmi}),
...(feature.properties.nst && { nst: feature.properties.nst}),
...(feature.properties.dmin && { dmin: feature.properties.dmin}),
...(feature.properties.gap && { gap: feature.properties.gap}),
},
}));
}
if (request.params.name === "count-earthquakes") {
const params = request.params.arguments as EarthquakeParams;
// Create a record of params, converting camelCase to snake_case where needed
const paramMap: Record<string, string | number | undefined> = {
endtime: params.endTime,
starttime: params.startTime,
minlatitude: params.minLatitude,
maxlatitude: params.maxLatitude,
minlongitude: params.minLongitude,
maxlongitude: params.maxLongitude,
latitude: params.latitude,
longitude: params.longitude,
maxradiuskm: params.maxradiuskm,
limit: params.limit,
maxdepth: params.maxdepth,
maxmagnitude: params.maxmagnitude,
mindepth: params.mindepth,
minmagnitude: params.minmagnitude,
orderby: params.orderby,
// alertlevel: params.alertlevel,
};
// Filter out undefined values and convert to URLSearchParams compatible format
const searchParams = new URLSearchParams(
Object.entries(paramMap)
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => [key, value!.toString()]),
);
const response = await fetch(
`https://earthquake.usgs.gov/fdsnws/event/1/count?format=geojson&${searchParams}`,
);
if (!response.ok) {
throw new Error(
`Failed to count earthquakes: ${response.statusText} when using params:${searchParams.toString()}`,
);
}
data = await response.json();
}
if (request.params.name === "find-earthquake-details") {
const { eventid } = request.params.arguments as { eventid: string };
const response = await fetch(
`https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&eventid=${eventid}`,
);
if (!response.ok) {
throw new Error(
`Failed to fetch earthquake details: ${response.statusText} for eventid: ${eventid}`,
);
}
data = await response.json();
// Clean up the properties
const props = data.properties;
data = {
magnitude: props.mag,
magType: props.magType,
location: props.place,
time: props.time,
updated: props.updated,
felt: props.felt,
type: props.type,
title: props.title,
url: props.url,
tsunami: props.tsunami,
net: props.net,
status: props.status,
rms: props.rms,
// Only include these if they exist and aren't null
...(props.alert && { alert: props.alert }),
...(props.cdi && { cdi: props.cdi }),
...(props.mmi && { mmi: props.mmi }),
...(props.nst && { nst: props.nst }),
...(props.dmin && { dmin: props.dmin }),
...(props.gap && { gap: props.gap }),
...(props.products.origin && {
origin: {
properties: {
azimuthalGap: props.products.origin[0].properties["azimuthal-gap"],
depth: props.products.origin[0].properties.depth,
depthType: props.products.origin[0].properties["depth-type"],
horizontalError: props.products.origin[0].properties["horizontal-error"],
verticalError: props.products.origin[0].properties["vertical-error"],
magnitudeError: props.products.origin[0].properties["magnitude-error"],
magnitudeAzimuthalGap: props.products.origin[0].properties["magnitude-azimuthal-gap"],
magnitudeNumStations: props.products.origin[0].properties["magnitude-num-stations-used"],
magnitudeSource: props.products.origin[0].properties["magnitude-source"],
magnitudeType: props.products.origin[0].properties["magnitude-type"],
numStationsUsed: props.products.origin[0].properties["num-stations-used"],
numPhasesUsed: props.products.origin[0].properties["num-phases-used"],
minimumDistance: props.products.origin[0].properties["minimum-distance"],
standardError: props.products.origin[0].properties["standard-error"],
reviewStatus: props.products.origin[0].properties["review-status"]
}
}
}),
geometry: data.geometry
};
}
return {
content: [
{ type: "text", text: JSON.stringify(data, null, 2) } as TextContent,
],
};
});
// function validateCoordinates(lat: number, lng: number) {
// if (typeof lat !== "number" || typeof lng !== "number") {
// throw new Error("Coordinates must be numbers");
// }
// if (lat < -90 || lat > 90) {
// throw new Error("Latitude must be between -90 and 90 degrees");
// }
// if (lng < -180 || lng > 180) {
// throw new Error("Longitude must be between -180 and 180 degrees");
// }
// }
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err) => {
console.error("Error starting server:", err);
process.exit(1);
});