1
0
forked from baron/baron-sso
Files
baron-sso/mcp/hydra-mcp/src/index.js

325 lines
8.2 KiB
JavaScript
Executable File

#!/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);
});