@getagenthook/mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js ADDED
@@ -0,0 +1,115 @@
1
+ // Bearer HTTP client — ported 1:1 from packages/cli/src/http.ts + run.ts.
2
+ // Standalone: the published package cannot runtime-import @getagenthook/core,
3
+ // so the minimal wire types are mirrored here (packages/core/contract.ts).
4
+ import { randomUUID } from "node:crypto";
5
+ const BASE_PATH = "/api/v1";
6
+ const DEFAULT_API_URL = "https://getagenthook.com";
7
+ const DEFAULT_TIMEOUT_MS = 30_000;
8
+ const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
9
+ // One transient re-submit with the SAME idempotency key (never double-charge —
10
+ // the server dedupes on the key). Mirrors cli/src/commands/run.ts.
11
+ const SUBMIT_RETRIES = 1;
12
+ export class ApiError extends Error {
13
+ status;
14
+ code;
15
+ details;
16
+ retryAfter;
17
+ constructor(message, status, // 0 = network/timeout, no HTTP response
18
+ code, details, retryAfter) {
19
+ super(message);
20
+ this.status = status;
21
+ this.code = code;
22
+ this.details = details;
23
+ this.retryAfter = retryAfter;
24
+ this.name = "ApiError";
25
+ }
26
+ }
27
+ /** Resolution order mirrors the CLI: AGENTHOOK_API_URL env, then default.
28
+ * Enforces https:// so a hostile/misconfigured env can't redirect the Bearer
29
+ * key to a cleartext or attacker-controlled host (and poison the fetched tool
30
+ * descriptions). http:// is allowed ONLY for localhost, for local dev. */
31
+ export function resolveApiUrl() {
32
+ const raw = (process.env.AGENTHOOK_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
33
+ let u;
34
+ try {
35
+ u = new URL(raw);
36
+ }
37
+ catch {
38
+ throw new Error(`AGENTHOOK_API_URL is not a valid URL: ${raw}`);
39
+ }
40
+ const isLocalhost = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1";
41
+ if (u.protocol !== "https:" && !(u.protocol === "http:" && isLocalhost)) {
42
+ throw new Error(`AGENTHOOK_API_URL must use https:// (got ${u.protocol}//${u.hostname}). http:// is allowed only for localhost.`);
43
+ }
44
+ return raw;
45
+ }
46
+ /** AGENTHOOK_API_KEY env only (no on-disk credentials in the MCP server). */
47
+ export function resolveApiKey() {
48
+ return process.env.AGENTHOOK_API_KEY || undefined;
49
+ }
50
+ /** Bounded JSON request against <apiUrl>/api/v1<pathname>. Checks res.ok before
51
+ * res.json() (donor rule) and normalizes every failure to ApiError. */
52
+ async function api(apiUrl, pathname, opts = {}) {
53
+ const url = new URL(apiUrl + BASE_PATH + pathname);
54
+ const headers = { ...opts.headers };
55
+ if (opts.key)
56
+ headers.authorization = `Bearer ${opts.key}`;
57
+ if (opts.body !== undefined)
58
+ headers["content-type"] = "application/json";
59
+ let res;
60
+ try {
61
+ res = await fetch(url, {
62
+ method: opts.method ?? "GET",
63
+ headers,
64
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
65
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
66
+ });
67
+ }
68
+ catch (e) {
69
+ throw new ApiError(`request to ${url.host} failed: ${e.message}`, 0);
70
+ }
71
+ if (!res.ok) {
72
+ let body;
73
+ try {
74
+ body = (await res.json());
75
+ }
76
+ catch {
77
+ // non-JSON error body — fall through to the status line
78
+ }
79
+ throw new ApiError(body?.error ?? `HTTP ${res.status} ${res.statusText}`, res.status, body?.code, body?.details, body?.retry_after);
80
+ }
81
+ return (await res.json());
82
+ }
83
+ /** GET /api/v1/tools — the single live schema source. */
84
+ export async function fetchTools() {
85
+ const res = await api(resolveApiUrl(), "/tools", { key: resolveApiKey() });
86
+ return res.tools;
87
+ }
88
+ /** POST /api/v1/tools/:tool/run — submit, retrying only transient network
89
+ * failures (status 0) with the SAME idempotency key so a hiccup mid-submit
90
+ * never double-charges. */
91
+ export async function submitRun(tool, input) {
92
+ const apiUrl = resolveApiUrl();
93
+ const key = resolveApiKey();
94
+ const idempotencyKey = randomUUID();
95
+ for (let attempt = 0;; attempt++) {
96
+ try {
97
+ return await api(apiUrl, `/tools/${encodeURIComponent(tool)}/run`, {
98
+ method: "POST",
99
+ key,
100
+ body: input,
101
+ headers: { [IDEMPOTENCY_KEY_HEADER]: idempotencyKey },
102
+ });
103
+ }
104
+ catch (e) {
105
+ if (e instanceof ApiError && e.status === 0 && attempt < SUBMIT_RETRIES)
106
+ continue;
107
+ throw e;
108
+ }
109
+ }
110
+ }
111
+ /** GET /api/v1/runs/:id — a SINGLE status read (the MCP client polls per call,
112
+ * there is no loop here — cf. cli/src/commands/run.ts which loops). */
113
+ export async function getRun(id) {
114
+ return api(resolveApiUrl(), `/runs/${encodeURIComponent(id)}`, { key: resolveApiKey() });
115
+ }
package/dist/main.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // Bin entry for `npx -y @getagenthook/mcp`. All diagnostics go to stderr —
3
+ // stdout carries ONLY JSON-RPC frames on stdio.
4
+ import { startServer } from "./server.js";
5
+ startServer().catch((err) => {
6
+ console.error(err instanceof Error ? err.message : String(err));
7
+ process.exit(1);
8
+ });
package/dist/schema.js ADDED
@@ -0,0 +1,36 @@
1
+ export function toJsonSchema(params) {
2
+ const properties = {};
3
+ const required = [];
4
+ for (const [name, spec] of Object.entries(params)) {
5
+ const prop = { type: spec.type };
6
+ if (spec.description !== undefined)
7
+ prop.description = spec.description;
8
+ if (spec.enum !== undefined)
9
+ prop.enum = [...spec.enum];
10
+ if (spec.type === "array" && spec.items)
11
+ prop.items = { type: spec.items.type };
12
+ if (spec.default !== undefined)
13
+ prop.default = spec.default;
14
+ if (spec.type === "number") {
15
+ if (spec.min !== undefined)
16
+ prop.minimum = spec.min;
17
+ if (spec.max !== undefined)
18
+ prop.maximum = spec.max;
19
+ }
20
+ else if (spec.type === "array") {
21
+ if (spec.min !== undefined)
22
+ prop.minItems = spec.min;
23
+ if (spec.max !== undefined)
24
+ prop.maxItems = spec.max;
25
+ }
26
+ if (spec.maxLength !== undefined)
27
+ prop.maxLength = spec.maxLength;
28
+ properties[name] = prop;
29
+ if (spec.required)
30
+ required.push(name);
31
+ }
32
+ const schema = { type: "object", properties, additionalProperties: false };
33
+ if (required.length)
34
+ schema.required = required;
35
+ return schema;
36
+ }
package/dist/server.js ADDED
@@ -0,0 +1,112 @@
1
+ // Stdio MCP server: a thin 1:1 proxy over the AgentHook REST API.
2
+ //
3
+ // DEVIATION FROM SPEC §5 (flagged): the spec names the high-level
4
+ // `McpServer.registerTool`, but in @modelcontextprotocol/sdk v1.29.0 that API
5
+ // accepts ONLY a Zod schema for `inputSchema` and converts it to JSON Schema
6
+ // internally — it cannot consume a pre-built JSON Schema (a raw JSON object is
7
+ // coerced to an empty schema; verified against the SDK source). The spec's
8
+ // load-bearing requirement (deliverables #3/#4: tool inputSchemas built LIVE
9
+ // from GET /tools via toJsonSchema, so they never drift) is only satisfiable by
10
+ // serving that JSON Schema verbatim — which the low-level `Server` does. So we
11
+ // use `Server` + ListTools/CallTool handlers. Same SDK, same transport; the
12
+ // only difference is toJsonSchema's output IS the wire inputSchema, unmodified.
13
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
14
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
16
+ import { ApiError, fetchTools, getRun, resolveApiKey, submitRun } from "./client.js";
17
+ import { toJsonSchema } from "./schema.js";
18
+ const GET_RUN_TOOL = "get_run";
19
+ const VERSION = "1.0.0";
20
+ // structuredContent contracts (advertised as each tool's outputSchema).
21
+ const RUN_CREATED_OUTPUT_SCHEMA = {
22
+ type: "object",
23
+ properties: {
24
+ run_id: { type: "string", description: "Poll this id with get_run for status + output URLs." },
25
+ credits_charged: { type: "number" },
26
+ },
27
+ required: ["run_id", "credits_charged"],
28
+ additionalProperties: false,
29
+ };
30
+ const GET_RUN_INPUT_SCHEMA = {
31
+ type: "object",
32
+ properties: {
33
+ run_id: { type: "string", description: "The run id returned by a generation tool." },
34
+ },
35
+ required: ["run_id"],
36
+ additionalProperties: false,
37
+ };
38
+ const GET_RUN_OUTPUT_SCHEMA = {
39
+ type: "object",
40
+ properties: {
41
+ status: { type: "string", description: "queued | processing | completed | failed" },
42
+ output: { type: "array", items: { type: "string" }, description: "Output URLs when completed." },
43
+ error: { type: ["string", "null"] },
44
+ },
45
+ required: ["status", "output"],
46
+ additionalProperties: false,
47
+ };
48
+ /** Build the (fully wired) MCP server. Reads AGENTHOOK_API_KEY and fails loud
49
+ * BEFORE any network call if it is missing; then fetches the live tool schema
50
+ * and registers the 4 generation tools + get_run. Throws on failure — the
51
+ * caller (main.ts) writes the message to stderr and exits non-zero. */
52
+ export async function buildServer() {
53
+ if (!resolveApiKey()) {
54
+ throw new Error("AGENTHOOK_API_KEY is not set. Add your key (ah_...) to the MCP server's env. " +
55
+ "Get one at https://getagenthook.com.");
56
+ }
57
+ const tools = await fetchTools();
58
+ const generationTools = new Set(tools.map((t) => t.name));
59
+ const toolList = [
60
+ ...tools.map((t) => ({
61
+ name: t.name,
62
+ description: t.description,
63
+ inputSchema: toJsonSchema(t.params),
64
+ outputSchema: RUN_CREATED_OUTPUT_SCHEMA,
65
+ })),
66
+ {
67
+ name: GET_RUN_TOOL,
68
+ description: "Fetch the status and output URLs of a run created by a generation tool. Poll this until status is 'completed' or 'failed'.",
69
+ inputSchema: GET_RUN_INPUT_SCHEMA,
70
+ outputSchema: GET_RUN_OUTPUT_SCHEMA,
71
+ },
72
+ ];
73
+ const server = new Server({ name: "agenthook", version: VERSION }, { capabilities: { tools: {} } });
74
+ server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: toolList }));
75
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
76
+ const { name } = request.params;
77
+ const args = (request.params.arguments ?? {});
78
+ try {
79
+ if (name === GET_RUN_TOOL) {
80
+ const run = await getRun(String(args.run_id ?? ""));
81
+ const structured = { status: run.status, output: run.output, error: run.error };
82
+ return { structuredContent: structured, content: [{ type: "text", text: JSON.stringify(structured) }] };
83
+ }
84
+ if (generationTools.has(name)) {
85
+ const created = await submitRun(name, args);
86
+ const structured = { run_id: created.run_id, credits_charged: created.credits_charged };
87
+ return {
88
+ structuredContent: structured,
89
+ content: [
90
+ {
91
+ type: "text",
92
+ text: `Run ${created.run_id} submitted (${created.credits_charged} credits charged). Call get_run with this run_id to fetch the result.`,
93
+ },
94
+ ],
95
+ };
96
+ }
97
+ throw new Error(`Unknown tool: ${name}`);
98
+ }
99
+ catch (e) {
100
+ // Surface API/errors to the agent as a tool error (not a protocol crash).
101
+ const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : String(e);
102
+ return { isError: true, content: [{ type: "text", text: msg }] };
103
+ }
104
+ });
105
+ return server;
106
+ }
107
+ /** Boot: build the server and connect it to stdio. */
108
+ export async function startServer() {
109
+ const server = await buildServer();
110
+ await server.connect(new StdioServerTransport());
111
+ console.error("agenthook MCP server ready on stdio.");
112
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@getagenthook/mcp",
3
+ "version": "1.0.0",
4
+ "description": "Local stdio MCP server for the AgentHook hosted media-generation API — make character-consistent UGC video, images, and captions from any MCP client.",
5
+ "license": "Apache-2.0",
6
+ "homepage": "https://getagenthook.com",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/AvivK5498/agenthook.git"
10
+ },
11
+ "mcpName": "io.github.AvivK5498/agenthook",
12
+ "keywords": [
13
+ "mcp",
14
+ "modelcontextprotocol",
15
+ "ai",
16
+ "video-generation",
17
+ "image-generation",
18
+ "agents"
19
+ ],
20
+ "type": "module",
21
+ "bin": {
22
+ "agenthook-mcp": "./dist/main.js"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.build.json",
32
+ "prepack": "npm run build",
33
+ "test": "vitest run",
34
+ "typecheck": "tsc --noEmit"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.29.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.10.2",
41
+ "typescript": "^5.6.3",
42
+ "vitest": "^2.1.8"
43
+ }
44
+ }