@zhook/mcp-server 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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2024 Zhook
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @zhook/mcp-server
2
+
3
+ The official Model Context Protocol (MCP) server for [Zhook](https://zhook.dev), enabling AI agents to interact with your Zhook webhooks, events, and metrics.
4
+
5
+ ## Features
6
+
7
+ - **List Hooks**: Retrieve a list of your configured webhooks.
8
+ - **Inspect Events**: View recent events for a specific hook.
9
+ - **Wait for Event**: Pause execution and wait for a specific event to occur.
10
+ - **Metrics**: Access detailed metrics about your webhook performance.
11
+
12
+ ## Installation
13
+
14
+ You can run this server using `npx` or install it globally.
15
+
16
+ ### Using `npx` (Recommended for Claude Desktop)
17
+
18
+ Add the following to your MCP configuration (e.g., `claude_desktop_config.json`):
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "zhook": {
24
+ "command": "npx",
25
+ "args": [
26
+ "-y",
27
+ "@zhook/mcp-server"
28
+ ],
29
+ "env": {
30
+ "ZHOOK_API_KEY": "your_api_key_here"
31
+ }
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ ### Manual Installation
38
+
39
+ ```bash
40
+ npm install -g @zhook/mcp-server
41
+ ```
42
+
43
+ Then run it:
44
+
45
+ ```bash
46
+ zhook-mcp
47
+ ```
48
+
49
+ ## Configuration
50
+
51
+ The server requires a Zhook API key to authenticate requests. You can obtain this key from your Zhook dashboard.
52
+
53
+ | Environment Variable | Description | Required |
54
+ |----------------------|-------------|----------|
55
+ | `ZHOOK_API_KEY` | Your Zhook API Key | Yes |
56
+
57
+ ## Tools
58
+
59
+ - `zhook_list_hooks`: List all available hooks in your account.
60
+ - `zhook_get_events`: Get recent events for a hook.
61
+ - `zhook_wait_for_event`: Wait for a specific event trigger.
62
+
63
+ ## License
64
+
65
+ ISC
@@ -0,0 +1,67 @@
1
+ import { ZhookClient } from './zhook-client.js';
2
+ import * as dotenv from 'dotenv';
3
+ dotenv.config();
4
+ async function main() {
5
+ console.log("🚀 Starting Zhook MCP Server Dry Run...");
6
+ const apiKey = process.env.ZHOOK_API_KEY;
7
+ if (!apiKey) {
8
+ console.error("❌ Error: ZHOOK_API_KEY is not set in environment or .env file.");
9
+ process.exit(1);
10
+ }
11
+ const client = new ZhookClient();
12
+ try {
13
+ // 1. List Hooks
14
+ console.log("\n1️⃣ Listing Hooks...");
15
+ const hooks = await client.listHooks();
16
+ console.log(` ✅ Found ${hooks.hooks.length} hooks.`);
17
+ // 2. Create a Test Hook
18
+ console.log("\n2️⃣ Creating Test Hook...");
19
+ const newHook = await client.createHook({
20
+ type: 'standard',
21
+ deliveryMethod: 'http',
22
+ metadata: {
23
+ name: 'MCP Dry Run Hook',
24
+ test: true
25
+ }
26
+ });
27
+ console.log(` ✅ Created Hook: ${newHook.hookId} (${newHook.url})`);
28
+ // 3. Add a Destination
29
+ console.log("\n3️⃣ Adding Destination...");
30
+ const destination = await client.createDestination(newHook.hookId, {
31
+ type: 'http',
32
+ config: {
33
+ url: 'https://httpbin.org/post',
34
+ method: 'POST'
35
+ },
36
+ name: 'HttpBin Test'
37
+ });
38
+ console.log(` ✅ Added Destination: ${destination.destinationId}`);
39
+ // 4. List Destinations
40
+ console.log("\n4️⃣ Listing Destinations...");
41
+ const destinations = await client.listDestinations(newHook.hookId);
42
+ console.log(` ✅ Found ${destinations.destinations.length} destinations.`);
43
+ // 5. Create Transformation (JSONata)
44
+ console.log("\n5️⃣ Creating Transformation...");
45
+ const transformation = await client.createTransformation(newHook.hookId, {
46
+ name: 'Simple Pass-through',
47
+ code: '$'
48
+ });
49
+ console.log(` ✅ Created Transformation: ${transformation.transformationId}`);
50
+ // 6. Get Metrics (Empty but checks endpoint)
51
+ console.log("\n6️⃣ Fetching Metrics...");
52
+ await client.getHookMetrics(newHook.hookId);
53
+ console.log(` ✅ Metrics fetched successfully.`);
54
+ // 7. Clean up
55
+ console.log("\n7️⃣ Cleaning up (Deleting Hook)...");
56
+ await client.request('DELETE', `/hooks/${newHook.hookId}`); // Using raw request as deleteHook might not be strictly exposed in top-level tool yet or I missed it in ZhookClient
57
+ console.log(` ✅ Hook deleted.`);
58
+ console.log("\n🎉 Dry Run Completed Successfully!");
59
+ }
60
+ catch (error) {
61
+ console.error("\n❌ Test Failed:", error.message);
62
+ if (error.response) {
63
+ console.error(" API Response:", JSON.stringify(error.response.data, null, 2));
64
+ }
65
+ }
66
+ }
67
+ main();
package/dist/index.js ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ // 1. SILENCE STDOUT IMMEDIATELY
3
+ // Redirect console.log to console.error to keep stdout pure for JSON-RPC
4
+ const originalLog = console.log;
5
+ console.log = console.error;
6
+ // 2. Imports (Dynamic to ensure silence first)
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { zodToJsonSchema } from "zod-to-json-schema";
10
+ async function main() {
11
+ // Dynamic imports to prevent top-level side effects from polluting stdout
12
+ // We import these HERE, after the console override is active.
13
+ const HookTools = await import('./tools/hooks.js');
14
+ const EventTools = await import('./tools/events.js');
15
+ const DestinationTools = await import('./tools/destinations.js');
16
+ const TransformationTools = await import('./tools/transformations.js');
17
+ const MetricTools = await import('./tools/metrics.js');
18
+ const WebhookTools = await import('./tools/webhooks.js');
19
+ // Create server instance
20
+ const server = new McpServer({
21
+ name: "zhook-mcp-server",
22
+ version: "1.0.0",
23
+ });
24
+ // Helper to convert Zod schema to clean JSON Schema for MCP
25
+ const toMcpSchema = (schema) => {
26
+ const jsonSchema = zodToJsonSchema(schema);
27
+ // Remove $schema field as it can cause validation issues in some SDK versions if mismatch
28
+ if ('$schema' in jsonSchema) {
29
+ delete jsonSchema['$schema'];
30
+ }
31
+ return jsonSchema;
32
+ };
33
+ // Register Webhook Test Tool
34
+ // Register Webhook Test Tool using explicit registerTool structure to avoid ambiguity
35
+ // and ensuring we pass the Zod schema directly.
36
+ console.error("Registering trigger_webhook with inputSchema:", WebhookTools.triggerWebhookTool.inputSchema);
37
+ server.registerTool(WebhookTools.triggerWebhookTool.name, {
38
+ description: WebhookTools.triggerWebhookTool.description,
39
+ inputSchema: WebhookTools.triggerWebhookTool.inputSchema,
40
+ }, WebhookTools.triggerWebhookTool.handler);
41
+ // Register Hook Tools
42
+ // Register Hook Tools
43
+ server.registerTool(HookTools.listHooksTool.name, {
44
+ description: HookTools.listHooksTool.description,
45
+ inputSchema: HookTools.listHooksTool.inputSchema,
46
+ }, HookTools.listHooksTool.handler);
47
+ server.registerTool(HookTools.createHookTool.name, {
48
+ description: HookTools.createHookTool.description,
49
+ inputSchema: HookTools.createHookTool.inputSchema,
50
+ }, HookTools.createHookTool.handler);
51
+ server.registerTool(HookTools.getHookTool.name, {
52
+ description: HookTools.getHookTool.description,
53
+ inputSchema: HookTools.getHookTool.inputSchema,
54
+ }, HookTools.getHookTool.handler);
55
+ // Register Destination Tools
56
+ server.registerTool(DestinationTools.listDestinationsTool.name, {
57
+ description: DestinationTools.listDestinationsTool.description,
58
+ inputSchema: DestinationTools.listDestinationsTool.inputSchema,
59
+ }, DestinationTools.listDestinationsTool.handler);
60
+ server.registerTool(DestinationTools.createDestinationTool.name, {
61
+ description: DestinationTools.createDestinationTool.description,
62
+ inputSchema: DestinationTools.createDestinationTool.inputSchema,
63
+ }, DestinationTools.createDestinationTool.handler);
64
+ server.registerTool(DestinationTools.getDestinationTool.name, {
65
+ description: DestinationTools.getDestinationTool.description,
66
+ inputSchema: DestinationTools.getDestinationTool.inputSchema,
67
+ }, DestinationTools.getDestinationTool.handler);
68
+ server.registerTool(DestinationTools.updateDestinationTool.name, {
69
+ description: DestinationTools.updateDestinationTool.description,
70
+ inputSchema: DestinationTools.updateDestinationTool.inputSchema,
71
+ }, DestinationTools.updateDestinationTool.handler);
72
+ server.registerTool(DestinationTools.deleteDestinationTool.name, {
73
+ description: DestinationTools.deleteDestinationTool.description,
74
+ inputSchema: DestinationTools.deleteDestinationTool.inputSchema,
75
+ }, DestinationTools.deleteDestinationTool.handler);
76
+ // Register Transformation Tools
77
+ server.registerTool(TransformationTools.listTransformationsTool.name, {
78
+ description: TransformationTools.listTransformationsTool.description,
79
+ inputSchema: TransformationTools.listTransformationsTool.inputSchema,
80
+ }, TransformationTools.listTransformationsTool.handler);
81
+ server.registerTool(TransformationTools.createTransformationTool.name, {
82
+ description: TransformationTools.createTransformationTool.description,
83
+ inputSchema: TransformationTools.createTransformationTool.inputSchema,
84
+ }, TransformationTools.createTransformationTool.handler);
85
+ server.registerTool(TransformationTools.updateTransformationTool.name, {
86
+ description: TransformationTools.updateTransformationTool.description,
87
+ inputSchema: TransformationTools.updateTransformationTool.inputSchema,
88
+ }, TransformationTools.updateTransformationTool.handler);
89
+ server.registerTool(TransformationTools.deleteTransformationTool.name, {
90
+ description: TransformationTools.deleteTransformationTool.description,
91
+ inputSchema: TransformationTools.deleteTransformationTool.inputSchema,
92
+ }, TransformationTools.deleteTransformationTool.handler);
93
+ // Register Metric Tools
94
+ server.registerTool(MetricTools.getHookMetricsTool.name, {
95
+ description: MetricTools.getHookMetricsTool.description,
96
+ inputSchema: MetricTools.getHookMetricsTool.inputSchema,
97
+ }, MetricTools.getHookMetricsTool.handler);
98
+ server.registerTool(MetricTools.getAggregatedHookMetricsTool.name, {
99
+ description: MetricTools.getAggregatedHookMetricsTool.description,
100
+ inputSchema: MetricTools.getAggregatedHookMetricsTool.inputSchema,
101
+ }, MetricTools.getAggregatedHookMetricsTool.handler);
102
+ // Register Event Tools
103
+ server.registerTool(EventTools.listEventsTool.name, {
104
+ description: EventTools.listEventsTool.description,
105
+ inputSchema: EventTools.listEventsTool.inputSchema,
106
+ }, EventTools.listEventsTool.handler);
107
+ server.registerTool(EventTools.getEventTool.name, {
108
+ description: EventTools.getEventTool.description,
109
+ inputSchema: EventTools.getEventTool.inputSchema,
110
+ }, EventTools.getEventTool.handler);
111
+ server.registerTool(EventTools.waitForEventTool.name, {
112
+ description: EventTools.waitForEventTool.description,
113
+ inputSchema: EventTools.waitForEventTool.inputSchema,
114
+ }, EventTools.waitForEventTool.handler);
115
+ const transport = new StdioServerTransport();
116
+ await server.connect(transport);
117
+ }
118
+ main().catch((error) => {
119
+ process.exit(1);
120
+ });
@@ -0,0 +1,24 @@
1
+ import { ZhookClient } from './zhook-client.js';
2
+ import * as dotenv from 'dotenv';
3
+ dotenv.config();
4
+ async function main() {
5
+ const client = new ZhookClient();
6
+ try {
7
+ console.log("Fetching hooks...");
8
+ const result = await client.listHooks();
9
+ console.log("--- Your Hooks ---");
10
+ if (result.hooks && result.hooks.length > 0) {
11
+ result.hooks.forEach((h) => {
12
+ console.log(`- [${h.hookId}] ${h.metadata?.name || 'Unnamed'} (${h.url}) - Active: ${h.active}`);
13
+ });
14
+ }
15
+ else {
16
+ console.log("No hooks found.");
17
+ }
18
+ console.log("------------------");
19
+ }
20
+ catch (error) {
21
+ console.error("Error fetching hooks:", error.message);
22
+ }
23
+ }
24
+ main();
@@ -0,0 +1,111 @@
1
+ import { z } from 'zod';
2
+ import { ZhookClient } from '../zhook-client.js';
3
+ const client = new ZhookClient();
4
+ export const listDestinationsTool = {
5
+ name: "list_destinations",
6
+ description: "List all destinations configured for a specific webhook.",
7
+ inputSchema: z.object({
8
+ hookId: z.string().describe("The ID of the hook to list destinations for")
9
+ }),
10
+ handler: async (args) => {
11
+ const result = await client.listDestinations(args.hookId);
12
+ return {
13
+ content: [{
14
+ type: "text",
15
+ text: JSON.stringify(result, null, 2)
16
+ }]
17
+ };
18
+ }
19
+ };
20
+ export const getDestinationTool = {
21
+ name: "get_destination",
22
+ description: "Get detailed configuration for a specific destination.",
23
+ inputSchema: z.object({
24
+ hookId: z.string().describe("The ID of the hook"),
25
+ destinationId: z.string().describe("The ID of the destination")
26
+ }),
27
+ handler: async (args) => {
28
+ const result = await client.getDestination(args.hookId, args.destinationId);
29
+ return {
30
+ content: [{
31
+ type: "text",
32
+ text: JSON.stringify(result, null, 2)
33
+ }]
34
+ };
35
+ }
36
+ };
37
+ const destinationConfigSchema = z.object({
38
+ type: z.enum(['http', 'mqtt', 'email']).describe("Type of destination"),
39
+ config: z.object({
40
+ // HTTP
41
+ url: z.string().url().optional().describe("Target URL (HTTP only)"),
42
+ method: z.enum(['GET', 'POST', 'PUT', 'PATCH']).optional().default('POST').describe("HTTP Method"),
43
+ headers: z.record(z.string(), z.string()).optional().describe("HTTP Headers"),
44
+ // MQTT
45
+ brokerUrl: z.string().optional().describe("MQTT Broker URL (e.g., mqtt://broker.hivemq.com)"),
46
+ topic: z.string().optional().describe("MQTT Topic"),
47
+ qos: z.number().min(0).max(2).optional().default(0),
48
+ // Email
49
+ email: z.string().email().optional().describe("Email address to send to")
50
+ }).describe("Configuration object specific to the destination type"),
51
+ name: z.string().optional().describe("Friendly name for this destination")
52
+ });
53
+ export const createDestinationTool = {
54
+ name: "create_destination",
55
+ description: "Add a new destination to a hook to forward events to.",
56
+ inputSchema: z.object({
57
+ hookId: z.string().describe("The ID of the hook"),
58
+ ...destinationConfigSchema.shape
59
+ }),
60
+ handler: async (args) => {
61
+ const payload = {
62
+ type: args.type,
63
+ config: args.config,
64
+ name: args.name
65
+ };
66
+ const result = await client.createDestination(args.hookId, payload);
67
+ return {
68
+ content: [{
69
+ type: "text",
70
+ text: JSON.stringify(result, null, 2)
71
+ }]
72
+ };
73
+ }
74
+ };
75
+ export const updateDestinationTool = {
76
+ name: "update_destination",
77
+ description: "Update an existing destination's configuration.",
78
+ inputSchema: z.object({
79
+ hookId: z.string(),
80
+ destinationId: z.string(),
81
+ ...destinationConfigSchema.partial().shape,
82
+ active: z.boolean().optional().describe("Enable or disable this destination")
83
+ }),
84
+ handler: async (args) => {
85
+ const { hookId, destinationId, ...data } = args;
86
+ const result = await client.updateDestination(hookId, destinationId, data);
87
+ return {
88
+ content: [{
89
+ type: "text",
90
+ text: JSON.stringify(result, null, 2)
91
+ }]
92
+ };
93
+ }
94
+ };
95
+ export const deleteDestinationTool = {
96
+ name: "delete_destination",
97
+ description: "Remove a destination from a hook.",
98
+ inputSchema: z.object({
99
+ hookId: z.string(),
100
+ destinationId: z.string()
101
+ }),
102
+ handler: async (args) => {
103
+ await client.deleteDestination(args.hookId, args.destinationId);
104
+ return {
105
+ content: [{
106
+ type: "text",
107
+ text: `Destination ${args.destinationId} deleted successfully.`
108
+ }]
109
+ };
110
+ }
111
+ };
@@ -0,0 +1,136 @@
1
+ import { z } from 'zod';
2
+ import { ZhookClient } from '../zhook-client.js';
3
+ import { ZhookClient as RealtimeClient } from '@zhook/client';
4
+ const apiClient = new ZhookClient();
5
+ export const listEventsTool = {
6
+ name: "list_events",
7
+ description: "List recent events received by a specific hook. Useful for checking what payloads have been delivered.",
8
+ inputSchema: z.object({
9
+ hookId: z.string().describe("The ID of the hook to inspect"),
10
+ limit: z.number().max(50).default(5).describe("Number of events to return (max 50, default 5)")
11
+ }),
12
+ handler: async (args) => {
13
+ const result = await apiClient.listEvents(args.hookId, args.limit);
14
+ // Summary view to save context tokens
15
+ const summary = result.events.map((e) => ({
16
+ eventId: e.eventId,
17
+ receivedAt: e.receivedAt,
18
+ method: e.method,
19
+ contentType: e.contentType,
20
+ size: e.contentLength
21
+ }));
22
+ return {
23
+ content: [{
24
+ type: "text",
25
+ text: JSON.stringify(summary, null, 2)
26
+ }]
27
+ };
28
+ }
29
+ };
30
+ export const getEventTool = {
31
+ name: "get_event",
32
+ description: "Get the full JSON payload and details of a specific event.",
33
+ inputSchema: z.object({
34
+ hookId: z.string().describe("The hook ID"),
35
+ eventId: z.string().describe("The event ID to retrieve")
36
+ }),
37
+ handler: async (args) => {
38
+ const result = await apiClient.getEvent(args.hookId, args.eventId);
39
+ return {
40
+ content: [{
41
+ type: "text",
42
+ text: JSON.stringify(result, null, 2)
43
+ }]
44
+ };
45
+ }
46
+ };
47
+ export const waitForEventTool = {
48
+ name: "wait_for_event",
49
+ description: "Connects to the Zhook WebSocket and waits for the NEXT event to arrive on a specific hook. Returns the full event payload immediately. TIMEOUT is 60 seconds.",
50
+ inputSchema: z.object({
51
+ hookId: z.string().describe("The hook ID to watch"),
52
+ timeoutSeconds: z.number().default(60).describe("How long to wait before giving up")
53
+ }),
54
+ handler: async (args) => {
55
+ const apiKey = process.env.ZHOOK_API_KEY;
56
+ if (!apiKey) {
57
+ throw new Error("ZHOOK_API_KEY not set");
58
+ }
59
+ let wsUrl = process.env.ZHOOK_WS_URL || 'wss://api.zhook.dev/events';
60
+ // Ensure URL ends with /events if it's the standard API domain
61
+ if (!wsUrl.endsWith('/events') && !wsUrl.endsWith('/')) {
62
+ wsUrl += '/events';
63
+ }
64
+ // Try to find a subscriber key for this hook
65
+ let clientKey = apiKey;
66
+ try {
67
+ const hookDetails = await apiClient.getHook(args.hookId);
68
+ if (hookDetails && hookDetails.keys && Array.isArray(hookDetails.keys) && hookDetails.keys.length > 0) {
69
+ clientKey = hookDetails.keys[0].key || hookDetails.keys[0];
70
+ }
71
+ else if (hookDetails && hookDetails.subscriberKey) {
72
+ clientKey = hookDetails.subscriberKey;
73
+ }
74
+ }
75
+ catch (err) {
76
+ console.error("Failed to fetch hook details for key lookup, using API Key", err);
77
+ }
78
+ return new Promise((resolve, reject) => {
79
+ // Initialize Realtime Client
80
+ // Corrected usage: RealtimeClient(clientKey, options)
81
+ const realtime = new RealtimeClient(clientKey, {
82
+ wsUrl: wsUrl,
83
+ project: 'default'
84
+ });
85
+ // Setup Timeout
86
+ const timer = setTimeout(() => {
87
+ try {
88
+ realtime.disconnect(); // Or close() if specific method name
89
+ // Based on source code it has 'close()' or 'disconnect' might mean close in logic
90
+ // Checking source: it has 'close()'. 'disconnect' is not a method on ZhookClient class.
91
+ realtime.close();
92
+ }
93
+ catch (e) { /* ignore */ }
94
+ resolve({
95
+ content: [{
96
+ type: "text",
97
+ text: "Timeout: No event received within " + args.timeoutSeconds + " seconds."
98
+ }]
99
+ });
100
+ }, args.timeoutSeconds * 1000);
101
+ // Handle Events
102
+ const onEvent = (data) => {
103
+ // We only care about events for THIS hook
104
+ if (data.hookId === args.hookId) {
105
+ clearTimeout(timer);
106
+ try {
107
+ realtime.close();
108
+ }
109
+ catch (e) { }
110
+ resolve({
111
+ content: [{
112
+ type: "text",
113
+ text: JSON.stringify(data, null, 2)
114
+ }]
115
+ });
116
+ }
117
+ };
118
+ // Setup Listeners
119
+ // Source says: onHookCalled(handler) for webhook events
120
+ // Source says: onConnected(handler), onError(handler)
121
+ // It does NOT have .on('event', ...). The class does NOT extend EventEmitter.
122
+ // It has .handlers array and explicit methods.
123
+ realtime.onHookCalled(onEvent);
124
+ realtime.onError((err) => {
125
+ // Silent error handling
126
+ });
127
+ // Connect
128
+ realtime.connect().catch((err) => {
129
+ clearTimeout(timer);
130
+ resolve({
131
+ content: [{ type: "text", text: "Error connecting to realtime: " + err.message }]
132
+ });
133
+ });
134
+ });
135
+ }
136
+ };
@@ -0,0 +1,89 @@
1
+ import { z } from 'zod';
2
+ import { ZhookClient } from '../zhook-client.js';
3
+ const client = new ZhookClient();
4
+ export const listHooksTool = {
5
+ name: "list_hooks",
6
+ description: "List all webhooks configured in the Zhook account. Returns hook IDs, URLs, and active status.",
7
+ inputSchema: z.object({}),
8
+ handler: async () => {
9
+ const result = await client.listHooks();
10
+ // Format for easier reading by LLM
11
+ const summarized = result.hooks.map((h) => ({
12
+ id: h.hookId,
13
+ name: h.metadata?.name || 'Unnamed Hook',
14
+ url: h.url,
15
+ type: h.type || 'standard',
16
+ deliveryMethod: h.deliveryMethod,
17
+ active: h.active,
18
+ status: h.status
19
+ }));
20
+ return {
21
+ content: [{
22
+ type: "text",
23
+ text: JSON.stringify(summarized, null, 2)
24
+ }]
25
+ };
26
+ }
27
+ };
28
+ export const getHookTool = {
29
+ name: "get_hook",
30
+ description: "Get detailed configuration for a specific webhook, including delivery URL, metadata, and recent metrics.",
31
+ inputSchema: z.object({
32
+ hookId: z.string().describe("The ID of the hook to retrieve (e.g. hook_abc123)")
33
+ }),
34
+ handler: async (args) => {
35
+ console.error('[GetHook] Args received:', JSON.stringify(args));
36
+ const result = await client.getHook(args.hookId);
37
+ return {
38
+ content: [{
39
+ type: "text",
40
+ text: JSON.stringify(result, null, 2)
41
+ }]
42
+ };
43
+ }
44
+ };
45
+ const createHookSchema = z.object({
46
+ type: z.enum(['standard', 'mqtt']).default('standard').describe("The type of hook to create. Use 'mqtt' to create an MQTT source hook."),
47
+ deliveryMethod: z.enum(['http', 'websocket', 'both']).default('websocket').describe("How you want to receive events. Defaults to 'websocket' for easy testing."),
48
+ callbackUrl: z.string().url().optional().describe("Required if deliveryMethod is 'http' or 'both'."),
49
+ metadata: z.record(z.string(), z.any()).optional().describe("Optional key-value metadata to attach to the hook (name, tags)."),
50
+ sourceConfig: z.object({
51
+ topic: z.string().describe("MQTT Topic to subscribe to (required for type=mqtt)"),
52
+ brokerUrl: z.string().optional().describe("MQTT Broker URL"),
53
+ username: z.string().optional(),
54
+ password: z.string().optional()
55
+ }).optional().describe("Configuration for MQTT source. Required if type is 'mqtt'.")
56
+ });
57
+ export const createHookTool = {
58
+ name: "create_hook",
59
+ description: "Create a new webhook or MQTT hook. Returns the new hook ID and its public URL.",
60
+ inputSchema: createHookSchema,
61
+ handler: async (args) => {
62
+ // Basic validation logic that might save an API call
63
+ if (args.type === 'mqtt' && !args.sourceConfig) {
64
+ throw new Error("sourceConfig is required when type is 'mqtt'");
65
+ }
66
+ // Explicitly handle defaults if Zod doesn't apply them automatically in the handler context
67
+ const deliveryMethod = args.deliveryMethod || 'websocket';
68
+ const type = args.type === 'standard' || !args.type ? 'http' : args.type;
69
+ const payload = {
70
+ type,
71
+ deliveryMethod,
72
+ callbackUrl: args.callbackUrl,
73
+ metadata: args.metadata,
74
+ sourceConfig: args.sourceConfig
75
+ };
76
+ // Fix for MQTT source config: map brokerUrl -> url
77
+ if (type === 'mqtt' && payload.sourceConfig && payload.sourceConfig.brokerUrl) {
78
+ payload.sourceConfig.url = payload.sourceConfig.brokerUrl;
79
+ delete payload.sourceConfig.brokerUrl;
80
+ }
81
+ const result = await client.createHook(payload);
82
+ return {
83
+ content: [{
84
+ type: "text",
85
+ text: JSON.stringify(result, null, 2)
86
+ }]
87
+ };
88
+ }
89
+ };
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ import { ZhookClient } from '../zhook-client.js';
3
+ const client = new ZhookClient();
4
+ export const getHookMetricsTool = {
5
+ name: "get_hook_metrics",
6
+ description: "Get real-time metrics for a specific hook (request counts, success/failure rates).",
7
+ inputSchema: z.object({
8
+ hookId: z.string().describe("The ID of the hook"),
9
+ timeWindow: z.enum(['hour', 'day', 'week']).default('hour').describe("Time window for metrics")
10
+ }),
11
+ handler: async (args) => {
12
+ const result = await client.getHookMetrics(args.hookId, args.timeWindow);
13
+ return {
14
+ content: [{
15
+ type: "text",
16
+ text: JSON.stringify(result, null, 2)
17
+ }]
18
+ };
19
+ }
20
+ };
21
+ export const getAggregatedHookMetricsTool = {
22
+ name: "get_aggregated_hook_metrics",
23
+ description: "Get historical aggregated metrics for a specific hook with custom date ranges.",
24
+ inputSchema: z.object({
25
+ hookId: z.string(),
26
+ startDate: z.string().optional().describe("ISO date string for start time"),
27
+ endDate: z.string().optional().describe("ISO date string for end time"),
28
+ groupBy: z.enum(['hour', 'day']).default('hour').describe("Granularity of aggregation")
29
+ }),
30
+ handler: async (args) => {
31
+ const { hookId, ...query } = args;
32
+ const result = await client.getAggregatedHookMetrics(args.hookId, query);
33
+ return {
34
+ content: [{
35
+ type: "text",
36
+ text: JSON.stringify(result, null, 2)
37
+ }]
38
+ };
39
+ }
40
+ };
@@ -0,0 +1,78 @@
1
+ import { z } from 'zod';
2
+ import { ZhookClient } from '../zhook-client.js';
3
+ const client = new ZhookClient();
4
+ export const listTransformationsTool = {
5
+ name: "list_transformations",
6
+ description: "List all transformations configured for a specific webhook.",
7
+ inputSchema: z.object({
8
+ hookId: z.string().describe("The ID of the hook")
9
+ }),
10
+ handler: async (args) => {
11
+ const result = await client.listTransformations(args.hookId);
12
+ return {
13
+ content: [{
14
+ type: "text",
15
+ text: JSON.stringify(result, null, 2)
16
+ }]
17
+ };
18
+ }
19
+ };
20
+ const transformationSchema = z.object({
21
+ name: z.string().describe("Name of the transformation"),
22
+ code: z.string().describe("JSONata transformation code"),
23
+ active: z.boolean().optional().default(true)
24
+ });
25
+ export const createTransformationTool = {
26
+ name: "create_transformation",
27
+ description: "Create a new JSONata transformation for a hook.",
28
+ inputSchema: z.object({
29
+ hookId: z.string(),
30
+ ...transformationSchema.shape
31
+ }),
32
+ handler: async (args) => {
33
+ const { hookId, ...data } = args;
34
+ const result = await client.createTransformation(hookId, data);
35
+ return {
36
+ content: [{
37
+ type: "text",
38
+ text: JSON.stringify(result, null, 2)
39
+ }]
40
+ };
41
+ }
42
+ };
43
+ export const updateTransformationTool = {
44
+ name: "update_transformation",
45
+ description: "Update an existing transformation.",
46
+ inputSchema: z.object({
47
+ hookId: z.string(),
48
+ transformationId: z.string(),
49
+ ...transformationSchema.partial().shape
50
+ }),
51
+ handler: async (args) => {
52
+ const { hookId, transformationId, ...data } = args;
53
+ const result = await client.updateTransformation(hookId, transformationId, data);
54
+ return {
55
+ content: [{
56
+ type: "text",
57
+ text: JSON.stringify(result, null, 2)
58
+ }]
59
+ };
60
+ }
61
+ };
62
+ export const deleteTransformationTool = {
63
+ name: "delete_transformation",
64
+ description: "Delete a transformation from a hook.",
65
+ inputSchema: z.object({
66
+ hookId: z.string(),
67
+ transformationId: z.string()
68
+ }),
69
+ handler: async (args) => {
70
+ await client.deleteTransformation(args.hookId, args.transformationId);
71
+ return {
72
+ content: [{
73
+ type: "text",
74
+ text: `Transformation ${args.transformationId} deleted successfully.`
75
+ }]
76
+ };
77
+ }
78
+ };
@@ -0,0 +1,77 @@
1
+ import { z } from 'zod';
2
+ import { ZhookClient } from '../zhook-client.js';
3
+ const apiClient = new ZhookClient();
4
+ const triggerWebhookSchema = z.object({
5
+ hookId: z.string().optional().describe("The ID of the hook to trigger (preferred)"),
6
+ hook_id: z.string().optional().describe("Alias for hookId"),
7
+ payload: z.string().describe("The payload to send. Can be a JSON object, a JSON string, or just plain text."),
8
+ contentType: z.enum(['application/json', 'text/plain']).default('application/json').describe("Content-Type of the webhook (default: application/json)")
9
+ });
10
+ export const triggerWebhookTool = {
11
+ name: "trigger_webhook",
12
+ description: "Send a test webhook event to a specific Hook. This mimics a real third-party service sending data to the hook URL.",
13
+ inputSchema: triggerWebhookSchema,
14
+ handler: async (args, extra) => {
15
+ console.error('[TriggerWebhook] First Arg (args):', JSON.stringify(args, null, 2));
16
+ console.error('[TriggerWebhook] Second Arg (extra):', JSON.stringify(extra, null, 2));
17
+ // Handle potentially shifted arguments (if args is actually extra, where is args?)
18
+ // If args has 'signal' etc, it's likely 'extra'.
19
+ let actualArgs = args;
20
+ if (args && args.sessionId && args.requestId) {
21
+ console.error('[TriggerWebhook] First arg looks like context/extra! Args might be missing or shifted.');
22
+ // If the first arg is extra, maybe the args are undefined/empty?
23
+ // Or maybe we are using the wrong handler signature for the defined schema?
24
+ }
25
+ const normalizedArgs = {};
26
+ if (actualArgs) {
27
+ for (const key of Object.keys(actualArgs)) {
28
+ normalizedArgs[key.toLowerCase()] = actualArgs[key];
29
+ }
30
+ }
31
+ const resolvedHookId = normalizedArgs.hookid || normalizedArgs.hook_id;
32
+ const resolvedPayload = normalizedArgs.payload;
33
+ const resolvedContentType = normalizedArgs.contenttype || 'application/json';
34
+ if (!resolvedHookId) {
35
+ return {
36
+ isError: true,
37
+ content: [{
38
+ type: "text",
39
+ text: `Failed to trigger webhook: hookId (or hook_id) is required. Received keys: ${Object.keys(args).join(', ')}`
40
+ }]
41
+ };
42
+ }
43
+ try {
44
+ const result = await apiClient.triggerWebhook(resolvedHookId, resolvedPayload, resolvedContentType);
45
+ return {
46
+ content: [{
47
+ type: "text",
48
+ text: JSON.stringify({
49
+ success: true,
50
+ status: result.status,
51
+ data: result.data
52
+ }, null, 2)
53
+ }]
54
+ };
55
+ }
56
+ catch (error) {
57
+ // Extract request details if available
58
+ const config = error.config || {};
59
+ const requestUrl = config.url || 'unknown';
60
+ const baseURL = config.baseURL || 'unknown';
61
+ const method = config.method || 'unknown';
62
+ return {
63
+ isError: true,
64
+ content: [{
65
+ type: "text",
66
+ text: `Failed to trigger webhook.
67
+ Error: ${error.message}
68
+ Status: ${error.response?.status} ${error.response?.statusText}
69
+ Method: ${method.toUpperCase()}
70
+ Attempted URL: ${requestUrl}
71
+ Base URL: ${baseURL}
72
+ Data: ${JSON.stringify(error.response?.data || {}, null, 2)}`
73
+ }]
74
+ };
75
+ }
76
+ }
77
+ };
@@ -0,0 +1,134 @@
1
+ import axios from 'axios';
2
+ import dotenv from 'dotenv';
3
+ // Load environment variables
4
+ dotenv.config();
5
+ // Zhook API Client
6
+ export class ZhookClient {
7
+ client;
8
+ constructor() {
9
+ const apiKey = process.env.ZHOOK_API_KEY;
10
+ if (!apiKey) {
11
+ throw new Error('ZHOOK_API_KEY environment variable is required');
12
+ }
13
+ const baseURL = process.env.ZHOOK_API_URL || 'https://api.zhook.dev/api/v1';
14
+ this.client = axios.create({
15
+ baseURL,
16
+ headers: {
17
+ 'Authorization': `Bearer ${apiKey}`,
18
+ 'Content-Type': 'application/json',
19
+ 'User-Agent': 'zhook-mcp-server/1.0.0'
20
+ }
21
+ });
22
+ }
23
+ // Generic request wrapper
24
+ async request(method, url, data, params) {
25
+ try {
26
+ const response = await this.client.request({
27
+ method,
28
+ url,
29
+ data,
30
+ params
31
+ });
32
+ return response.data;
33
+ }
34
+ catch (error) {
35
+ if (axios.isAxiosError(error) && error.response) {
36
+ throw new Error(`Zhook API Error: ${error.response.status} ${JSON.stringify(error.response.data)}`);
37
+ }
38
+ throw error;
39
+ }
40
+ }
41
+ // Hooks
42
+ // Destinations
43
+ async listDestinations(hookId) {
44
+ return this.request('GET', `/hooks/${hookId}/destinations`);
45
+ }
46
+ async createDestination(hookId, data) {
47
+ return this.request('POST', `/hooks/${hookId}/destinations`, data);
48
+ }
49
+ async getDestination(hookId, destinationId) {
50
+ return this.request('GET', `/hooks/${hookId}/destinations/${destinationId}`);
51
+ }
52
+ async updateDestination(hookId, destinationId, data) {
53
+ return this.request('PUT', `/hooks/${hookId}/destinations/${destinationId}`, data);
54
+ }
55
+ async deleteDestination(hookId, destinationId) {
56
+ return this.request('DELETE', `/hooks/${hookId}/destinations/${destinationId}`);
57
+ }
58
+ // Transformations
59
+ async listTransformations(hookId) {
60
+ return this.request('GET', `/hooks/${hookId}/transformations`);
61
+ }
62
+ async createTransformation(hookId, data) {
63
+ return this.request('POST', `/hooks/${hookId}/transformations`, data);
64
+ }
65
+ async updateTransformation(hookId, transformationId, data) {
66
+ return this.request('PUT', `/hooks/${hookId}/transformations/${transformationId}`, data);
67
+ }
68
+ async deleteTransformation(hookId, transformationId) {
69
+ return this.request('DELETE', `/hooks/${hookId}/transformations/${transformationId}`);
70
+ }
71
+ // Metrics
72
+ async getHookMetrics(hookId, timeWindow = 'hour') {
73
+ return this.request('GET', `/metrics/hooks/${hookId}?timeWindow=${timeWindow}`);
74
+ }
75
+ async getAggregatedHookMetrics(hookId, query) {
76
+ const params = new URLSearchParams(query);
77
+ return this.request('GET', `/metrics/hooks/${hookId}/aggregated?${params.toString()}`);
78
+ }
79
+ // Hooks
80
+ async listHooks() {
81
+ const result = await this.request('GET', '/hooks');
82
+ // Legacy domain fix: Ensure URLs use zhook.dev instead of hookr.cloud
83
+ if (result && result.hooks) {
84
+ result.hooks.forEach((h) => {
85
+ if (h.url && h.url.includes('hookr.cloud')) {
86
+ h.url = h.url.replace('hookr.cloud', 'zhook.dev');
87
+ }
88
+ if (h.callbackUrl && h.callbackUrl.includes('hookr.cloud')) {
89
+ h.callbackUrl = h.callbackUrl.replace('hookr.cloud', 'zhook.dev');
90
+ }
91
+ });
92
+ }
93
+ return result;
94
+ }
95
+ async getHook(hookId) {
96
+ const result = await this.request('GET', `/hooks/${hookId}`);
97
+ // Legacy domain fix
98
+ if (result) {
99
+ if (result.url && result.url.includes('hookr.cloud')) {
100
+ result.url = result.url.replace('hookr.cloud', 'zhook.dev');
101
+ }
102
+ if (result.callbackUrl && result.callbackUrl.includes('hookr.cloud')) {
103
+ result.callbackUrl = result.callbackUrl.replace('hookr.cloud', 'zhook.dev');
104
+ }
105
+ }
106
+ return result;
107
+ }
108
+ async createHook(data) {
109
+ return this.request('POST', '/hooks', data);
110
+ }
111
+ // Events
112
+ async listEvents(hookId, limit = 10) {
113
+ return this.request('GET', `/hooks/${hookId}/events`, undefined, { limit });
114
+ }
115
+ async getEvent(hookId, eventId) {
116
+ return this.request('GET', `/hooks/${hookId}/events/${eventId}`);
117
+ }
118
+ // Webhooks (Testing)
119
+ async triggerWebhook(hookId, payload, contentType = 'application/json') {
120
+ console.error(`[ZhookClient] triggerWebhook called with hookId: ${hookId}, payload: ${JSON.stringify(payload)}`);
121
+ const config = {
122
+ headers: {
123
+ 'Content-Type': contentType
124
+ }
125
+ };
126
+ // We need to hit the INGESTION endpoint (/h/:id) which is the public webhook receiver.
127
+ // /hooks/:id is primarily for GET (details) or requires correct routing in server.js.
128
+ // The user confirmed /h/:id is the correct path for POSTing webhooks.
129
+ const baseUrl = this.client.defaults.baseURL || '';
130
+ const rootUrl = baseUrl.replace(/\/api\/v1\/?$/, '');
131
+ // Use the short URL /h/:id
132
+ return this.client.post(`${rootUrl}/h/${hookId}`, payload, config);
133
+ }
134
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@zhook/mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP Server for Zhook - Manage webhooks and inspect events from your AI agent",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "zhook-mcp": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "zhook",
18
+ "webhook",
19
+ "ai",
20
+ "agent",
21
+ "llm",
22
+ "claude",
23
+ "chatgpt"
24
+ ],
25
+ "author": "Zhook",
26
+ "license": "ISC",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/zhookteam/zhook-mcp.git"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "homepage": "https://zhook.dev",
35
+ "bugs": {
36
+ "url": "https://github.com/zhookteam/zhook-mcp/issues"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.0.1",
40
+ "@zhook/client": "^1.0.0",
41
+ "axios": "^1.7.9",
42
+ "dotenv": "^16.4.5",
43
+ "zod": "^3.23.8",
44
+ "zod-to-json-schema": "^3.25.1"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^20.0.0",
48
+ "typescript": "^5.0.0"
49
+ },
50
+ "files": [
51
+ "dist"
52
+ ]
53
+ }