#!/usr/bin/env node import { createRequire } from "node:module"; import path from "node:path"; import { pathToFileURL } from "node:url"; const modulesBase = process.env.MCP_MODULES_DIR; const requireFromModules = createRequire( modulesBase ? path.join(modulesBase, "package.json") : import.meta.url, ); const mcpModule = await import(resolveModule("@modelcontextprotocol/sdk/server/mcp.js")); const stdioModule = await import(resolveModule("@modelcontextprotocol/sdk/server/stdio.js")); const zodModule = await import(resolveModule("zod")); const { McpServer } = mcpModule; const { StdioServerTransport } = stdioModule; const { z } = zodModule; const hydraPublicUrl = process.env.HYDRA_PUBLIC_URL ?? "http://127.0.0.1:4444"; const hydraAdminUrl = process.env.HYDRA_ADMIN_URL ?? "http://127.0.0.1:4445"; const adminApiToken = process.env.HYDRA_ADMIN_API_TOKEN; const publicApiToken = process.env.HYDRA_PUBLIC_API_TOKEN; const timeoutMs = Number.parseInt(process.env.HYDRA_HTTP_TIMEOUT_MS ?? "15000", 10); class HttpError extends Error { constructor(message, status, body, url) { super(message); this.name = "HttpError"; this.status = status; this.body = body; this.url = url; } } function resolveModule(specifier) { const resolvedPath = requireFromModules.resolve(specifier); return pathToFileURL(resolvedPath).href; } function buildUrl(base, path, query) { const url = new URL(path, base); if (query) { for (const [key, value] of Object.entries(query)) { if (value === undefined || value === null || value === "") { continue; } url.searchParams.set(key, String(value)); } } return url.toString(); } async function requestJson(url, { method = "GET", headers, body } = {}, token) { const controller = new AbortController(); const timeoutId = Number.isFinite(timeoutMs) ? setTimeout(() => controller.abort(), timeoutMs) : null; const requestHeaders = { accept: "application/json", ...headers, }; if (token) { requestHeaders.authorization = `Bearer ${token}`; } try { const response = await fetch(url, { method, headers: requestHeaders, body, signal: controller.signal, }); const contentType = response.headers.get("content-type") ?? ""; const text = await response.text(); const data = text ? contentType.includes("application/json") ? safeJsonParse(text) : text : null; if (!response.ok) { throw new HttpError(`HTTP ${response.status} ${response.statusText}`, response.status, data, url); } return { status: response.status, data, }; } finally { if (timeoutId) { clearTimeout(timeoutId); } } } function safeJsonParse(text) { try { return JSON.parse(text); } catch { return text; } } function formatToolResult(payload) { return { content: [ { type: "text", text: JSON.stringify(payload, null, 2), }, ], }; } function formatErrorResult(error) { if (error instanceof HttpError) { return formatToolResult({ error: { message: error.message, status: error.status, url: error.url, body: error.body, }, }); } return formatToolResult({ error: { message: error instanceof Error ? error.message : String(error), }, }); } const HealthInputSchema = z.object({ service: z.enum(["public", "admin"]).optional(), probe: z.enum(["alive", "ready"]).optional(), }); const ListClientsInputSchema = z.object({ limit: z.number().int().positive().max(500).optional(), offset: z.number().int().min(0).optional(), page_size: z.number().int().positive().max(500).optional(), page_token: z.string().min(1).optional(), }); const ClientIdInputSchema = z.object({ client_id: z.string().min(1), }); const ClientPayloadInputSchema = z.object({ client_id: z.string().min(1), payload: z.record(z.unknown()), }); const RegisterClientInputSchema = z.object({ payload: z.record(z.unknown()), }); async function main() { const server = new McpServer({ name: "mcp-ory-hydra", version: "0.1.0", }); server.tool( "hydra_health", "Check Hydra health using /health/alive or /health/ready on public/admin ports.", HealthInputSchema.shape, async (input) => { const service = input.service ?? "admin"; const probe = input.probe ?? "ready"; const base = service === "public" ? hydraPublicUrl : hydraAdminUrl; const url = buildUrl(base, `/health/${probe}`); try { const result = await requestJson(url, {}, service === "public" ? publicApiToken : adminApiToken); return formatToolResult({ service, probe, status: result.status, data: result.data, }); } catch (error) { return formatErrorResult(error); } }, ); server.tool( "hydra_list_clients", "List OAuth2 clients from Hydra Admin API.", ListClientsInputSchema.shape, async (input) => { const url = buildUrl(hydraAdminUrl, "/clients", { limit: input.limit, offset: input.offset, page_size: input.page_size, page_token: input.page_token, }); try { const result = await requestJson(url, {}, adminApiToken); return formatToolResult({ status: result.status, data: result.data, }); } catch (error) { return formatErrorResult(error); } }, ); server.tool( "hydra_get_client", "Get an OAuth2 client by client_id from Hydra Admin API.", ClientIdInputSchema.shape, async (input) => { const url = buildUrl(hydraAdminUrl, `/clients/${encodeURIComponent(input.client_id)}`); try { const result = await requestJson(url, {}, adminApiToken); return formatToolResult({ status: result.status, data: result.data, }); } catch (error) { return formatErrorResult(error); } }, ); server.tool( "hydra_register_client", "Register an OAuth2 client via Hydra public dynamic client registration endpoint.", RegisterClientInputSchema.shape, async (input) => { const url = buildUrl(hydraPublicUrl, "/oauth2/register"); try { const result = await requestJson( url, { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify(input.payload ?? {}), }, publicApiToken, ); return formatToolResult({ status: result.status, data: result.data, }); } catch (error) { return formatErrorResult(error); } }, ); server.tool( "hydra_update_client", "Update an OAuth2 client via Hydra Admin API.", ClientPayloadInputSchema.shape, async (input) => { const url = buildUrl(hydraAdminUrl, `/clients/${encodeURIComponent(input.client_id)}`); try { const result = await requestJson( url, { method: "PUT", headers: { "content-type": "application/json", }, body: JSON.stringify(input.payload ?? {}), }, adminApiToken, ); return formatToolResult({ status: result.status, data: result.data, }); } catch (error) { return formatErrorResult(error); } }, ); server.tool( "hydra_delete_client", "Delete an OAuth2 client via Hydra Admin API.", ClientIdInputSchema.shape, async (input) => { const url = buildUrl(hydraAdminUrl, `/clients/${encodeURIComponent(input.client_id)}`); try { const result = await requestJson( url, { method: "DELETE", }, adminApiToken, ); return formatToolResult({ status: result.status, data: result.data, }); } catch (error) { return formatErrorResult(error); } }, ); const transport = new StdioServerTransport(); await server.connect(transport); } main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });