import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
import { type, type Type } from "arktype";
import { logger } from "./logger";
// Default to HTTPS port, fallback to HTTP if specified
const USE_HTTP = process.env.OBSIDIAN_USE_HTTP === "true";
const PORT = USE_HTTP ? 27123 : 27124;
const PROTOCOL = USE_HTTP ? "http" : "https";
export const BASE_URL = `${PROTOCOL}://127.0.0.1:${PORT}`;
// Disable TLS certificate validation for local self-signed certificates
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
/**
* Makes a request to the Obsidian Local REST API with the provided path and optional request options.
* Automatically adds the required API key to the request headers.
* Throws an `McpError` if the API response is not successful.
*
* @param path - The path to the Obsidian API endpoint.
* @param init - Optional request options to pass to the `fetch` function.
* @returns The response from the Obsidian API.
*/
export async function makeRequest<
T extends
| Type<{}, {}>
| Type<null | undefined, {}>
| Type<{} | null | undefined, {}>,
>(schema: T, path: string, init?: RequestInit): Promise<T["infer"]> {
const API_KEY = process.env.OBSIDIAN_API_KEY;
if (!API_KEY) {
logger.error("OBSIDIAN_API_KEY environment variable is required", {
env: process.env,
});
throw new Error("OBSIDIAN_API_KEY environment variable is required");
}
const url = `${BASE_URL}${path}`;
const response = await fetch(url, {
...init,
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "text/markdown",
...init?.headers,
},
});
if (!response.ok) {
const error = await response.text();
const message = `${init?.method ?? "GET"} ${path} ${response.status}: ${error}`;
throw new McpError(ErrorCode.InternalError, message);
}
const isJSON = !!response.headers.get("Content-Type")?.includes("json");
const data = isJSON ? await response.json() : await response.text();
// 204 No Content responses should be validated as undefined
const validated = response.status === 204 ? undefined : schema(data);
if (validated instanceof type.errors) {
const stackError = new Error();
Error.captureStackTrace(stackError, makeRequest);
logger.error("Invalid response from Obsidian API", {
status: response.status,
error: validated.summary,
stack: stackError.stack,
data,
});
throw new McpError(
ErrorCode.InternalError,
`${init?.method ?? "GET"} ${path} ${response.status}: ${validated.summary}`,
);
}
return validated;
}