@huntapi/mcp 0.0.0-stage → 0.1.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HuntAPI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,150 @@
1
- # Temporary Holding Version
1
+ # HuntAPI MCP Server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Official [Model Context Protocol](https://modelcontextprotocol.io) server for the [HuntAPI](https://www.huntapi.com) media download API.
4
+
5
+ Download video and audio from public media URLs, or extract their metadata, with one async REST API.
6
+
7
+ It lets Claude, Cursor, VS Code, Windsurf and any MCP client call the API as tools. Runs over **stdio** (local) or **Streamable HTTP** (self-hosted).
8
+
9
+ ## Requirements
10
+
11
+ - Node.js 20 or newer
12
+ - a HuntAPI API key — [sign up](https://app.huntapi.com/register), then copy it from the [dashboard](https://app.huntapi.com)
13
+
14
+ ## Hosted server (recommended)
15
+
16
+ HuntAPI runs this server for you at **`https://mcp.huntapi.com/`** (Streamable HTTP). Send your API key in the `x-api-key` header; nothing to install.
17
+
18
+ ```sh
19
+ claude mcp add --transport http huntapi https://mcp.huntapi.com/ --header "x-api-key: YOUR_API_KEY"
20
+ ```
21
+
22
+ ```json
23
+ {
24
+ "mcpServers": {
25
+ "huntapi": {
26
+ "url": "https://mcp.huntapi.com/",
27
+ "headers": {
28
+ "x-api-key": "YOUR_API_KEY"
29
+ }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ The hosted server and this package expose the same API; run the package locally when you want stdio, your own network egress, or to self-host it.
36
+
37
+ ## Run it locally
38
+
39
+ ### Claude Code
40
+
41
+ ```sh
42
+ claude mcp add huntapi --env HUNTAPI_API_KEY=YOUR_API_KEY -- npx -y @huntapi/mcp
43
+ ```
44
+
45
+ ### Claude Desktop, Cursor, Windsurf and other clients
46
+
47
+ Add the server to the client's MCP configuration (`claude_desktop_config.json`, `.cursor/mcp.json`…):
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "huntapi": {
53
+ "command": "npx",
54
+ "args": ["-y", "@huntapi/mcp"],
55
+ "env": {
56
+ "HUNTAPI_API_KEY": "YOUR_API_KEY"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### VS Code
64
+
65
+ ```json
66
+ {
67
+ "servers": {
68
+ "huntapi": {
69
+ "type": "stdio",
70
+ "command": "npx",
71
+ "args": ["-y", "@huntapi/mcp"],
72
+ "env": {
73
+ "HUNTAPI_API_KEY": "${input:huntapi-api-key}"
74
+ }
75
+ }
76
+ },
77
+ "inputs": [
78
+ {
79
+ "type": "promptString",
80
+ "id": "huntapi-api-key",
81
+ "description": "HuntAPI API key",
82
+ "password": true
83
+ }
84
+ ]
85
+ }
86
+ ```
87
+
88
+ ### Claude Desktop extension
89
+
90
+ Download the `.mcpb` bundle from the releases page and open it; Claude Desktop asks for your API key.
91
+
92
+ ### Docker
93
+
94
+ ```sh
95
+ docker build -t huntapi-mcp .
96
+ docker run -i --rm -e HUNTAPI_API_KEY=YOUR_API_KEY huntapi-mcp
97
+ ```
98
+
99
+ ## Tools
100
+
101
+ | Tool | Description |
102
+ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | `audio_download` | Download audio from a URL via HuntAPI and receive a job id. |
104
+ | `jobs` | Poll status, progress (0–100), and result of an async HuntAPI job. |
105
+ | `media_metadata` | Extract metadata from a media URL without downloading the file. |
106
+ | `stats` | Infrastructure health for an engine or endpoint over the last hour. Not scoped to your account or tenant. Enabled per account: HTTP 403 means contact support to activate it. |
107
+ | `usage` | Retrieve real-time usage statistics and credit balance for your Huntapi account. |
108
+ | `video_download` | Video Download |
109
+ | `wait_for_job` | Wait for an async download job to finish. |
110
+
111
+ Every tool that calls the API consumes credits. JSON results are returned as text; images are returned inline and other files (PDF…) as embedded resources, up to 4 MB.
112
+
113
+ ## Streamable HTTP
114
+
115
+ ```sh
116
+ npx @huntapi/mcp --http --port 3000
117
+ ```
118
+
119
+ The endpoint is `POST /mcp` (stateless, JSON responses). Each client sends its own key in the `x-api-key` header (or `Authorization: Bearer <key>`); the server never uses a key of its own. It binds to `127.0.0.1` by default and rejects browser origins unless you allow them — put it behind TLS before exposing it.
120
+
121
+ `createHttpHandler()` is exported for Fetch-compatible runtimes (Cloudflare Workers, Deno, Bun):
122
+
123
+ ```ts
124
+ import { createHttpHandler } from "@huntapi/mcp";
125
+
126
+ export default { fetch: createHttpHandler({ allowedOrigins: [] }) };
127
+ ```
128
+
129
+ ## Security
130
+
131
+ - The API key is read from `HUNTAPI_API_KEY` (stdio) or the request headers (HTTP) and removed from error messages.
132
+ - Content fetched from websites is untrusted: the server instructions tell the model to treat it as data, not instructions.
133
+ - See [SECURITY.md](SECURITY.md) to report a vulnerability.
134
+
135
+ ## Development
136
+
137
+ ```sh
138
+ npm install
139
+ npm test
140
+ npm run build
141
+ npx @modelcontextprotocol/inspector node dist/stdio.js
142
+ ```
143
+
144
+ ## Built on the SDK
145
+
146
+ The server uses the official [Node.js SDK](https://github.com/huntapi/huntapi-node) (`@huntapi/sdk`).
147
+
148
+ ## License
149
+
150
+ MIT — see [LICENSE](LICENSE).
package/SECURITY.md ADDED
@@ -0,0 +1,16 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are released for the latest published version of the HuntAPI MCP server.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Please do not report security vulnerabilities through public GitHub issues.
10
+
11
+ Email **contact@huntapi.com** with a description of the issue, the affected version, and steps to reproduce. We acknowledge reports within three business days and keep you informed until the issue is resolved.
12
+
13
+ ## Handling API keys
14
+
15
+ - Keep your HuntAPI API key out of source control. Load it from the `HUNTAPI_API_KEY` environment variable or a secret manager.
16
+ - Rotate the key from the [HuntAPI dashboard](https://app.huntapi.com) if you believe it was exposed.
@@ -0,0 +1,3 @@
1
+ import type { EndpointEntry } from "./types.js";
2
+ export declare const MODE: "catalog" | "tools";
3
+ export declare const CATALOG: readonly EndpointEntry[];
@@ -0,0 +1,190 @@
1
+ // Generated from the HuntAPI OpenAPI document. Do not edit by hand.
2
+ export const MODE = "tools";
3
+ export const CATALOG = [
4
+ {
5
+ name: "audio_download",
6
+ title: "Audio Download",
7
+ path: "/v1/audio/download",
8
+ methods: ["GET", "POST"],
9
+ description: "Audio Download. Download audio from a URL via HuntAPI and receive a job id. Credit cost: 1 credit per call.",
10
+ params: [
11
+ {
12
+ name: "query",
13
+ type: "string",
14
+ required: true,
15
+ description: "URL to download audio from.",
16
+ },
17
+ {
18
+ name: "download_type",
19
+ type: "string",
20
+ required: false,
21
+ description: "Desired download type. Supported values: audio (default).",
22
+ default: "audio",
23
+ },
24
+ {
25
+ name: "max_duration",
26
+ type: "number",
27
+ required: false,
28
+ description: "Maximum duration of the downloaded content in seconds. If the audio is longer than this value, only the content up to this duration will be downloaded.",
29
+ },
30
+ {
31
+ name: "audio_quality",
32
+ type: "string",
33
+ required: false,
34
+ description: "Desired audio quality.",
35
+ default: "best",
36
+ },
37
+ {
38
+ name: "audio_format",
39
+ type: "string",
40
+ required: false,
41
+ description: "Desired output format for the audio.",
42
+ default: "mp3",
43
+ },
44
+ {
45
+ name: "webhook_url",
46
+ type: "string",
47
+ required: false,
48
+ description: "URL of the webhook where results will be sent. When specified, the system will attempt to send the response to this URL up to 3 times with a 5-second delay between each attempt. The results will be sent as a POST request.",
49
+ },
50
+ {
51
+ name: "webhook_authorization",
52
+ type: "string",
53
+ required: false,
54
+ description: "Authorization header for the webhook. This parameter allows you to add authentication to your webhook endpoint.",
55
+ },
56
+ {
57
+ name: "fields",
58
+ type: "string",
59
+ required: false,
60
+ description: "Comma-separated list of fields to keep in the response, with dot notation for nested fields (e.g. name,location.city). Does not change the credit cost.",
61
+ },
62
+ ],
63
+ },
64
+ {
65
+ name: "jobs",
66
+ title: "Jobs",
67
+ path: "/v1/jobs/{id}",
68
+ methods: ["GET"],
69
+ description: "Jobs. Poll status, progress (0–100), and result of an async HuntAPI job.",
70
+ params: [
71
+ {
72
+ name: "id",
73
+ type: "string",
74
+ required: true,
75
+ description: "Job UUID returned as job_id by async endpoints such as /v1/video/download and /v1/audio/download.",
76
+ },
77
+ ],
78
+ },
79
+ {
80
+ name: "media_metadata",
81
+ title: "Media Metadata",
82
+ path: "/v1/media/metadata",
83
+ methods: ["GET"],
84
+ description: "Media Metadata. Extract metadata from a media URL without downloading the file. Credit cost: 1 credit per call.",
85
+ params: [
86
+ {
87
+ name: "query",
88
+ type: "string",
89
+ required: true,
90
+ description: "Media URL to inspect (YouTube, Instagram, TikTok, Vimeo, X, Facebook, Twitch, Reddit, etc.).",
91
+ },
92
+ {
93
+ name: "fields",
94
+ type: "string",
95
+ required: false,
96
+ description: "Comma-separated list of fields to keep in the response, with dot notation for nested fields (e.g. name,location.city). Does not change the credit cost.",
97
+ },
98
+ ],
99
+ },
100
+ {
101
+ name: "stats",
102
+ title: "Stats",
103
+ path: "/v1/stats",
104
+ methods: ["GET"],
105
+ description: "Stats. Infrastructure health for an engine or endpoint over the last hour. Not scoped to your account or tenant. Enabled per account: HTTP 403 means contact support to activate it.",
106
+ params: [
107
+ {
108
+ name: "engine",
109
+ type: "string",
110
+ required: false,
111
+ description: "API family to check, for example video. Provide at least engine or path.",
112
+ },
113
+ {
114
+ name: "path",
115
+ type: "string",
116
+ required: false,
117
+ description: "Specific endpoint to check, for example /v1/video/download. If you also pass engine, use an endpoint from that API.",
118
+ },
119
+ ],
120
+ },
121
+ {
122
+ name: "usage",
123
+ title: "Usage",
124
+ path: "/v1/usage",
125
+ methods: ["GET"],
126
+ description: "Usage. Retrieve real-time usage statistics and credit balance for your Huntapi account.",
127
+ params: [],
128
+ },
129
+ {
130
+ name: "video_download",
131
+ title: "Video Download",
132
+ path: "/v1/video/download",
133
+ methods: ["GET", "POST"],
134
+ description: "Video Download. Credit cost: 1 credit per call.",
135
+ params: [
136
+ {
137
+ name: "query",
138
+ type: "string",
139
+ required: true,
140
+ description: "Video URL to download.",
141
+ },
142
+ {
143
+ name: "download_type",
144
+ type: "string",
145
+ required: false,
146
+ description: "Desired download type. You have three options:",
147
+ default: "audio_video",
148
+ },
149
+ {
150
+ name: "max_duration",
151
+ type: "number",
152
+ required: false,
153
+ description: "Maximum duration of the downloaded content in seconds. If the video/audio is longer than this value, only the content up to this duration will be downloaded.",
154
+ },
155
+ {
156
+ name: "video_quality",
157
+ type: "string",
158
+ required: false,
159
+ description: "Desired video quality. Available options:",
160
+ default: "best",
161
+ },
162
+ {
163
+ name: "video_format",
164
+ type: "string",
165
+ required: false,
166
+ description: "Desired output format for the video. Available options:",
167
+ default: "mp4",
168
+ },
169
+ {
170
+ name: "webhook_url",
171
+ type: "string",
172
+ required: false,
173
+ description: "URL of the webhook where results will be sent. When specified, the system will attempt to send the response to this URL up to 3 times with a 5-second delay between each attempt. The results will be sent as a POST request.",
174
+ },
175
+ {
176
+ name: "webhook_authorization",
177
+ type: "string",
178
+ required: false,
179
+ description: "Authorization header for the webhook. This parameter allows you to add authentication to your webhook endpoint. This value will be included in the Authorization header when making the webhook request to your server.",
180
+ },
181
+ {
182
+ name: "fields",
183
+ type: "string",
184
+ required: false,
185
+ description: "Comma-separated list of fields to keep in the response, with dot notation for nested fields (e.g. name,location.city). Does not change the credit cost.",
186
+ },
187
+ ],
188
+ },
189
+ ];
190
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"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"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { type HuntApiClientOptions } from "@huntapi/sdk";
2
+ import { type ToolClient } from "./server.js";
3
+ export declare const MCP_PATH = "/mcp";
4
+ export declare const DEFAULT_MAX_BODY_BYTES: number;
5
+ export interface HttpHandlerOptions {
6
+ /** Origins accepted when a browser sends an Origin header. Empty rejects every browser origin. */
7
+ readonly allowedOrigins?: readonly string[];
8
+ readonly maxBodyBytes?: number;
9
+ /** Builds the SDK client for one request; override in tests. */
10
+ readonly clientFactory?: (options: HuntApiClientOptions) => ToolClient;
11
+ }
12
+ /**
13
+ * Fetch-style handler for a stateless, authenticated Streamable HTTP endpoint. Each POST gets a fresh
14
+ * server and transport; the caller's API key comes from the x-api-key header or a Bearer token.
15
+ * Works on Node.js (see serve.ts), Cloudflare Workers, Deno, Bun and any Fetch-compatible runtime.
16
+ */
17
+ export declare function createHttpHandler(options?: HttpHandlerOptions): (request: Request) => Promise<Response>;
package/dist/http.js ADDED
@@ -0,0 +1,77 @@
1
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
2
+ import { HuntApiClient } from "@huntapi/sdk";
3
+ import { createHuntApiMcpServer } from "./server.js";
4
+ export const MCP_PATH = "/mcp";
5
+ export const DEFAULT_MAX_BODY_BYTES = 256 * 1024;
6
+ /**
7
+ * Fetch-style handler for a stateless, authenticated Streamable HTTP endpoint. Each POST gets a fresh
8
+ * server and transport; the caller's API key comes from the x-api-key header or a Bearer token.
9
+ * Works on Node.js (see serve.ts), Cloudflare Workers, Deno, Bun and any Fetch-compatible runtime.
10
+ */
11
+ export function createHttpHandler(options = {}) {
12
+ const allowedOrigins = new Set(options.allowedOrigins ?? []);
13
+ const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
14
+ const clientFactory = options.clientFactory ??
15
+ ((clientOptions) => new HuntApiClient(clientOptions));
16
+ return async (request) => {
17
+ const url = new URL(request.url);
18
+ if (url.pathname === "/health")
19
+ return Response.json({ status: "ok" });
20
+ if (url.pathname !== MCP_PATH)
21
+ return jsonError(404, "Not found.");
22
+ if (request.method !== "POST") {
23
+ return new Response(null, { status: 405, headers: { Allow: "POST" } });
24
+ }
25
+ const origin = request.headers.get("origin");
26
+ if (origin !== null && !allowedOrigins.has(origin))
27
+ return jsonError(403, "Origin not allowed.");
28
+ const apiKey = readApiKey(request.headers);
29
+ if (!apiKey) {
30
+ return new Response(JSON.stringify({
31
+ error: "Send your HuntAPI API key in the x-api-key header.",
32
+ }), {
33
+ status: 401,
34
+ headers: {
35
+ "content-type": "application/json",
36
+ "www-authenticate": 'Bearer realm="HuntAPI"',
37
+ },
38
+ });
39
+ }
40
+ const declaredLength = Number(request.headers.get("content-length") ?? "0");
41
+ if (declaredLength > maxBodyBytes)
42
+ return jsonError(413, "Request body too large.");
43
+ const body = await request.text();
44
+ if (new TextEncoder().encode(body).length > maxBodyBytes)
45
+ return jsonError(413, "Request body too large.");
46
+ const server = createHuntApiMcpServer({
47
+ client: clientFactory({ apiKey }),
48
+ sensitiveValues: [apiKey],
49
+ });
50
+ const transport = new WebStandardStreamableHTTPServerTransport({
51
+ sessionIdGenerator: undefined,
52
+ enableJsonResponse: true,
53
+ });
54
+ await server.connect(transport);
55
+ try {
56
+ return await transport.handleRequest(new Request(request.url, {
57
+ method: "POST",
58
+ headers: request.headers,
59
+ body,
60
+ }));
61
+ }
62
+ finally {
63
+ void server.close();
64
+ }
65
+ };
66
+ }
67
+ function readApiKey(headers) {
68
+ const direct = headers.get("x-api-key")?.trim();
69
+ if (direct)
70
+ return direct;
71
+ const match = /^Bearer\s+(\S+)\s*$/i.exec(headers.get("authorization") ?? "");
72
+ return match?.[1];
73
+ }
74
+ function jsonError(status, message) {
75
+ return Response.json({ error: message }, { status });
76
+ }
77
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export { CATALOG, MODE } from "./catalog.js";
2
+ export { createHttpHandler, MCP_PATH, type HttpHandlerOptions, } from "./http.js";
3
+ export { redact } from "./redaction.js";
4
+ export { MAX_INLINE_BYTES, MAX_TEXT_CHARS, toErrorResult, toToolResult, } from "./result.js";
5
+ export { describeEndpoint, inputObject, inputShape } from "./schema.js";
6
+ export { startHttpServer, type ServeOptions } from "./serve.js";
7
+ export { createHuntApiMcpServer, SERVER_INSTRUCTIONS, SERVER_NAME, type ServerDependencies, type ToolClient, } from "./server.js";
8
+ export type { EndpointEntry, ParamEntry, ParamType } from "./types.js";
9
+ export { VERSION } from "./version.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { CATALOG, MODE } from "./catalog.js";
2
+ export { createHttpHandler, MCP_PATH, } from "./http.js";
3
+ export { redact } from "./redaction.js";
4
+ export { MAX_INLINE_BYTES, MAX_TEXT_CHARS, toErrorResult, toToolResult, } from "./result.js";
5
+ export { describeEndpoint, inputObject, inputShape } from "./schema.js";
6
+ export { startHttpServer } from "./serve.js";
7
+ export { createHuntApiMcpServer, SERVER_INSTRUCTIONS, SERVER_NAME, } from "./server.js";
8
+ export { VERSION } from "./version.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EACL,iBAAiB,EACjB,QAAQ,GAET,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,YAAY,GACb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,eAAe,EAAqB,MAAM,YAAY,CAAC;AAChE,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,WAAW,GAGZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,2 @@
1
+ /** Removes secrets from text before it reaches a model or a log. */
2
+ export declare function redact(text: string, sensitiveValues: readonly (string | null | undefined)[]): string;
@@ -0,0 +1,10 @@
1
+ /** Removes secrets from text before it reaches a model or a log. */
2
+ export function redact(text, sensitiveValues) {
3
+ let out = text;
4
+ for (const value of sensitiveValues) {
5
+ if (value && value.length >= 4)
6
+ out = out.split(value).join("[REDACTED]");
7
+ }
8
+ return out;
9
+ }
10
+ //# sourceMappingURL=redaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redaction.js","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,MAAM,UAAU,MAAM,CACpB,IAAY,EACZ,eAAuD;IAEvD,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,KAAK,MAAM,KAAK,IAAI,eAAe,EAAE,CAAC;QACpC,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC;YAAE,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ import { type ApiResponse } from "@huntapi/sdk";
3
+ /** Largest JSON text returned to the model, in characters. */
4
+ export declare const MAX_TEXT_CHARS = 200000;
5
+ /** Largest file returned inline (image or embedded resource), in bytes. */
6
+ export declare const MAX_INLINE_BYTES: number;
7
+ export declare function toToolResult(response: ApiResponse, path: string): CallToolResult;
8
+ export declare function toErrorResult(error: unknown, sensitiveValues: readonly (string | null | undefined)[]): CallToolResult;
package/dist/result.js ADDED
@@ -0,0 +1,103 @@
1
+ import { HuntApiApiError, HuntApiError, HuntApiSerializationError, } from "@huntapi/sdk";
2
+ import { redact } from "./redaction.js";
3
+ /** Largest JSON text returned to the model, in characters. */
4
+ export const MAX_TEXT_CHARS = 200_000;
5
+ /** Largest file returned inline (image or embedded resource), in bytes. */
6
+ export const MAX_INLINE_BYTES = 4 * 1024 * 1024;
7
+ const INLINE_IMAGE_TYPES = new Set([
8
+ "image/png",
9
+ "image/jpeg",
10
+ "image/webp",
11
+ "image/gif",
12
+ ]);
13
+ export function toToolResult(response, path) {
14
+ if (response.kind === "json") {
15
+ const text = JSON.stringify(response.data, null, 2) ?? "null";
16
+ return {
17
+ content: [
18
+ {
19
+ type: "text",
20
+ text: text.length > MAX_TEXT_CHARS
21
+ ? `${text.slice(0, MAX_TEXT_CHARS)}\n… (truncated: ${text.length} characters in total)`
22
+ : text,
23
+ },
24
+ ],
25
+ };
26
+ }
27
+ const mimeType = (response.contentType ?? "application/octet-stream")
28
+ .split(";", 1)[0]
29
+ .trim()
30
+ .toLowerCase();
31
+ const size = response.bytes.length;
32
+ const summary = {
33
+ type: "text",
34
+ text: `${mimeType} response from ${path} (${size} bytes).`,
35
+ };
36
+ if (size > MAX_INLINE_BYTES) {
37
+ return {
38
+ content: [
39
+ {
40
+ ...summary,
41
+ text: `${summary.text} Too large to return inline; call the API directly or use the SDK.`,
42
+ },
43
+ ],
44
+ };
45
+ }
46
+ const data = Buffer.from(response.bytes).toString("base64");
47
+ if (INLINE_IMAGE_TYPES.has(mimeType)) {
48
+ return { content: [{ type: "image", data, mimeType }, summary] };
49
+ }
50
+ const uri = `huntapi://response${path}`;
51
+ if (mimeType.startsWith("text/") ||
52
+ mimeType === "image/svg+xml" ||
53
+ mimeType.endsWith("+xml")) {
54
+ return {
55
+ content: [
56
+ {
57
+ type: "resource",
58
+ resource: {
59
+ uri,
60
+ mimeType,
61
+ text: Buffer.from(response.bytes).toString("utf8"),
62
+ },
63
+ },
64
+ summary,
65
+ ],
66
+ };
67
+ }
68
+ return {
69
+ content: [
70
+ { type: "resource", resource: { uri, mimeType, blob: data } },
71
+ summary,
72
+ ],
73
+ };
74
+ }
75
+ export function toErrorResult(error, sensitiveValues) {
76
+ let text;
77
+ if (error instanceof HuntApiApiError) {
78
+ const body = error.responseBody === undefined
79
+ ? ""
80
+ : `\n${JSON.stringify(error.responseBody).slice(0, 2_000)}`;
81
+ text = `HuntAPI API error ${error.status}: ${error.message}${body}`;
82
+ if (error.status === 401)
83
+ text += "\nCheck the HUNTAPI_API_KEY API key.";
84
+ if (error.status === 402)
85
+ text += "\nThe account has no credits left.";
86
+ if (error.status === 429)
87
+ text += "\nRate limit exceeded; retry later.";
88
+ }
89
+ else if (error instanceof HuntApiSerializationError) {
90
+ text = `Invalid parameters: ${error.message}`;
91
+ }
92
+ else if (error instanceof HuntApiError) {
93
+ text = `${error.name}: ${error.message}`;
94
+ }
95
+ else {
96
+ text = `Unexpected error: ${error instanceof Error ? error.message : String(error)}`;
97
+ }
98
+ return {
99
+ isError: true,
100
+ content: [{ type: "text", text: redact(text, sensitiveValues) }],
101
+ };
102
+ }
103
+ //# sourceMappingURL=result.js.map