Skip to content

Commit 0f41304

Browse files
committed
v2.1.0: Add 3 new batch/utility tools
- send_multiple_requests: Batch send for load testing - clone_webhook: Copy webhook with all settings - export_webhook_data: Export requests to JSON - Updated README with new tools (24 total)
1 parent d544305 commit 0f41304

File tree

8 files changed

+329
-2
lines changed

8 files changed

+329
-2
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.1.0] - 2026-01-26
9+
10+
### Added
11+
12+
- **3 New Tools** (21 → 24 total):
13+
- `send_multiple_requests` - Send batch of requests for load testing
14+
- `clone_webhook` - Copy webhook with all settings to a new token
15+
- `export_webhook_data` - Export all requests to JSON format
16+
- `post_raw()` method in HTTP client for absolute URLs
17+
18+
### Changed
19+
20+
- Updated README badge (21 → 24 tools)
21+
- Added "Batch & Utility" tools section to README
22+
823
## [2.0.7] - 2026-01-26
924

1025
### Changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![PyPI](https://img.shields.io/pypi/v/webhook-mcp-server.svg)](https://pypi.org/project/webhook-mcp-server/)
44
[![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/)
5-
[![MCP](https://img.shields.io/badge/MCP-21%20tools-brightgreen.svg)](https://modelcontextprotocol.io/)
5+
[![MCP](https://img.shields.io/badge/MCP-24%20tools-brightgreen.svg)](https://modelcontextprotocol.io/)
66
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
77

88
A Model Context Protocol (MCP) server for [webhook.site](https://webhook.site) - instantly capture HTTP requests, emails, and DNS lookups. Perfect for testing webhooks, debugging API callbacks, security testing, and bug bounty hunting.
@@ -134,6 +134,14 @@ Add to `claude_desktop_config.json`:
134134
| `check_for_callbacks` | Quick check for OOB callbacks |
135135
| `extract_links_from_request` | Extract URLs from captured requests |
136136

137+
### Batch & Utility
138+
139+
| Tool | Description |
140+
| ------------------------ | ---------------------------------------------- |
141+
| `send_multiple_requests` | Send batch of requests for load testing |
142+
| `clone_webhook` | Copy webhook with all settings |
143+
| `export_webhook_data` | Export all requests to JSON |
144+
137145
---
138146

139147
## Examples

handlers/tool_handlers.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ def _get_handler(
118118
"generate_xss_callback": self._handle_generate_xss_callback,
119119
"generate_canary_token": self._handle_generate_canary_token,
120120
"extract_links_from_request": self._handle_extract_links_from_request,
121+
# Batch & utility tools
122+
"send_multiple_requests": self._handle_send_multiple_requests,
123+
"clone_webhook": self._handle_clone_webhook,
124+
"export_webhook_data": self._handle_export_webhook_data,
121125
}
122126
return handlers.get(name)
123127

@@ -370,3 +374,31 @@ async def _handle_extract_links_from_request(self, arguments: dict[str, Any]):
370374
request_id=arguments.get("request_id"),
371375
filter_domain=arguments.get("filter_domain"),
372376
)
377+
378+
# =========================================================================
379+
# Batch & Utility Tool Handlers
380+
# =========================================================================
381+
382+
async def _handle_send_multiple_requests(self, arguments: dict[str, Any]) -> ToolResult:
383+
"""Handle send_multiple_requests tool."""
384+
self._validate_webhook_token(arguments["webhook_token"])
385+
return await self._request_service.send_multiple(
386+
webhook_token=arguments["webhook_token"],
387+
payloads=arguments["payloads"],
388+
delay_ms=arguments.get("delay_ms", 0),
389+
)
390+
391+
async def _handle_clone_webhook(self, arguments: dict[str, Any]) -> ToolResult:
392+
"""Handle clone_webhook tool."""
393+
self._validate_webhook_token(arguments["source_token"])
394+
return await self._webhook_service.clone_webhook(
395+
source_token=arguments["source_token"],
396+
)
397+
398+
async def _handle_export_webhook_data(self, arguments: dict[str, Any]) -> ToolResult:
399+
"""Handle export_webhook_data tool."""
400+
self._validate_webhook_token(arguments["webhook_token"])
401+
return await self._request_service.export_requests(
402+
webhook_token=arguments["webhook_token"],
403+
limit=arguments.get("limit", 100),
404+
)

models/schemas.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,4 +627,64 @@ def to_json(self) -> str:
627627
"required": ["webhook_token"]
628628
}
629629
),
630+
# =========================================================================
631+
# BATCH & UTILITY TOOLS
632+
# =========================================================================
633+
Tool(
634+
name="send_multiple_requests",
635+
description="Send multiple requests to a webhook in batch. Useful for load testing or sending multiple test payloads at once.",
636+
inputSchema={
637+
"type": "object",
638+
"properties": {
639+
"webhook_token": {
640+
"type": "string",
641+
"description": "The webhook token (UUID) from webhook.site"
642+
},
643+
"payloads": {
644+
"type": "array",
645+
"items": {"type": "object"},
646+
"description": "Array of JSON payloads to send to the webhook"
647+
},
648+
"delay_ms": {
649+
"type": "integer",
650+
"description": "Delay between requests in milliseconds (default: 0)",
651+
"default": 0
652+
}
653+
},
654+
"required": ["webhook_token", "payloads"]
655+
}
656+
),
657+
Tool(
658+
name="clone_webhook",
659+
description="Clone an existing webhook with all its settings. Creates a new webhook with the same configuration (status, content, CORS, timeout) as the source.",
660+
inputSchema={
661+
"type": "object",
662+
"properties": {
663+
"source_token": {
664+
"type": "string",
665+
"description": "The webhook token (UUID) to clone"
666+
}
667+
},
668+
"required": ["source_token"]
669+
}
670+
),
671+
Tool(
672+
name="export_webhook_data",
673+
description="Export all captured requests from a webhook to JSON format. Includes full request details: headers, body, IP, timestamp, user agent.",
674+
inputSchema={
675+
"type": "object",
676+
"properties": {
677+
"webhook_token": {
678+
"type": "string",
679+
"description": "The webhook token (UUID) from webhook.site"
680+
},
681+
"limit": {
682+
"type": "integer",
683+
"description": "Maximum number of requests to export (default: 100)",
684+
"default": 100
685+
}
686+
},
687+
"required": ["webhook_token"]
688+
}
689+
),
630690
]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "webhook-mcp-server"
3-
version = "2.0.7"
3+
version = "2.1.0"
44
description = "MCP Server for webhook.site API integration with layered architecture"
55
readme = "README.md"
66
license = {text = "MIT"}

services/request_service.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,118 @@ async def wait_for_email(
441441
}
442442
)
443443

444+
async def send_multiple(
445+
self,
446+
webhook_token: str,
447+
payloads: list[dict[str, Any]],
448+
delay_ms: int = 0,
449+
) -> ToolResult:
450+
"""Send multiple requests to a webhook in batch.
451+
452+
Useful for load testing or sending test payloads.
453+
454+
Args:
455+
webhook_token: The webhook UUID
456+
payloads: List of JSON payloads to send
457+
delay_ms: Delay between requests in milliseconds (default: 0)
458+
459+
Returns:
460+
ToolResult with success/failure counts
461+
"""
462+
from utils.http_client import WEBHOOK_SITE_API
463+
464+
results = []
465+
success_count = 0
466+
fail_count = 0
467+
468+
for i, payload in enumerate(payloads):
469+
try:
470+
response = await self._client.post_raw(
471+
f"{WEBHOOK_SITE_API}/{webhook_token}",
472+
json=payload
473+
)
474+
results.append({
475+
"index": i,
476+
"success": True,
477+
"status_code": response.status_code
478+
})
479+
success_count += 1
480+
except Exception as e:
481+
results.append({
482+
"index": i,
483+
"success": False,
484+
"error": str(e)
485+
})
486+
fail_count += 1
487+
488+
# Add delay between requests if specified
489+
if delay_ms > 0 and i < len(payloads) - 1:
490+
await asyncio.sleep(delay_ms / 1000.0)
491+
492+
return ToolResult(
493+
success=fail_count == 0,
494+
message=f"Sent {success_count}/{len(payloads)} requests successfully",
495+
data={
496+
"total": len(payloads),
497+
"success_count": success_count,
498+
"fail_count": fail_count,
499+
"results": results
500+
}
501+
)
502+
503+
async def export_requests(
504+
self,
505+
webhook_token: str,
506+
limit: int = 100,
507+
format: str = "json",
508+
) -> ToolResult:
509+
"""Export all requests from a webhook to JSON.
510+
511+
Args:
512+
webhook_token: The webhook UUID
513+
limit: Maximum number of requests to export (default: 100)
514+
format: Export format (currently only 'json' supported)
515+
516+
Returns:
517+
ToolResult with exported data
518+
"""
519+
data = await self._client.get(
520+
f"/token/{webhook_token}/requests",
521+
params={"per_page": limit},
522+
)
523+
524+
requests_data = data.get("data", [])
525+
526+
# Format each request with full details
527+
export_data = []
528+
for req in requests_data:
529+
export_data.append({
530+
"uuid": req.get("uuid"),
531+
"type": req.get("type"),
532+
"method": req.get("method"),
533+
"url": req.get("url"),
534+
"ip": req.get("ip"),
535+
"hostname": req.get("hostname"),
536+
"headers": req.get("headers", {}),
537+
"query": req.get("query", {}),
538+
"content": req.get("content"),
539+
"text_content": req.get("text_content"),
540+
"created_at": req.get("created_at"),
541+
"user_agent": self._extract_header(req, "User-Agent"),
542+
"content_type": self._extract_header(req, "Content-Type"),
543+
})
544+
545+
return ToolResult(
546+
success=True,
547+
message=f"Exported {len(export_data)} requests",
548+
data={
549+
"webhook_token": webhook_token,
550+
"export_format": format,
551+
"request_count": len(export_data),
552+
"requests": export_data
553+
}
554+
)
555+
444556
@staticmethod
445557
def _extract_header(req: dict[str, Any], header_name: str) -> str:
446558
"""Extract a header value from a request."""

services/webhook_service.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,72 @@ async def get_dns(self, webhook_token: str, validate: bool = False) -> ToolResul
351351
"url": url,
352352
}
353353
)
354+
355+
async def clone_webhook(self, source_token: str) -> ToolResult:
356+
"""Clone an existing webhook with all its settings.
357+
358+
Creates a new webhook with the same configuration as the source.
359+
360+
Args:
361+
source_token: The webhook UUID to clone
362+
363+
Returns:
364+
ToolResult with new webhook token and copied settings
365+
"""
366+
try:
367+
# Get source webhook info
368+
response = await self._client.get(f"/token/{source_token}")
369+
response.raise_for_status()
370+
source_data = response.json()
371+
372+
# Create new webhook with same config
373+
config = WebhookConfig(
374+
default_status=source_data.get("default_status"),
375+
default_content=source_data.get("default_content"),
376+
default_content_type=source_data.get("default_content_type"),
377+
timeout=source_data.get("timeout"),
378+
cors=source_data.get("cors"),
379+
)
380+
381+
# Create the cloned webhook
382+
create_response = await self._client.post("/token", json=config.to_payload())
383+
create_response.raise_for_status()
384+
new_data = create_response.json()
385+
386+
new_token = new_data.get("uuid")
387+
urls = self._build_webhook_urls(new_token)
388+
389+
return ToolResult(
390+
success=True,
391+
message=f"Webhook cloned! New URL: {urls['url']}",
392+
data={
393+
"source_token": source_token,
394+
"new_token": new_token,
395+
**urls,
396+
"cloned_settings": {
397+
"default_status": source_data.get("default_status"),
398+
"default_content": source_data.get("default_content"),
399+
"default_content_type": source_data.get("default_content_type"),
400+
"timeout": source_data.get("timeout"),
401+
"cors": source_data.get("cors"),
402+
}
403+
}
404+
)
405+
except WebhookApiError as e:
406+
if e.status_code == 404:
407+
return ToolResult(
408+
success=False,
409+
message=f"Source token '{source_token}' not found or expired",
410+
data=None
411+
)
412+
return ToolResult(
413+
success=False,
414+
message=f"Failed to clone webhook: {str(e)}",
415+
data=None
416+
)
417+
except Exception as e:
418+
return ToolResult(
419+
success=False,
420+
message=f"Failed to clone webhook: {str(e)}",
421+
data=None
422+
)

utils/http_client.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,34 @@ async def delete(
226226
return response.status_code
227227
except httpx.RequestError as e:
228228
raise WebhookApiError(f"DELETE request failed: {str(e)}") from e
229+
230+
async def post_raw(
231+
self,
232+
url: str,
233+
json: dict[str, Any] | None = None,
234+
headers: dict[str, str] | None = None,
235+
) -> httpx.Response:
236+
"""Perform POST request to an absolute URL (not using base_url).
237+
238+
Useful for posting to webhook endpoints directly.
239+
240+
Args:
241+
url: Full URL to POST to
242+
json: JSON body data
243+
headers: Additional headers
244+
245+
Returns:
246+
Full httpx Response object
247+
248+
Raises:
249+
WebhookApiError: On HTTP or API errors
250+
"""
251+
try:
252+
response = await self.client.post(
253+
url,
254+
json=json,
255+
headers=headers,
256+
)
257+
return response
258+
except httpx.RequestError as e:
259+
raise WebhookApiError(f"POST request failed: {str(e)}") from e

0 commit comments

Comments
 (0)