feat: initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
89
README.md
Normal file
89
README.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# Fetch MCP Server
|
||||||
|
|
||||||
|
This MCP server provides functionality to fetch web content in various formats, including HTML, JSON, plain text, and Markdown.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
- **fetch_html**
|
||||||
|
- Fetch a website and return the content as HTML
|
||||||
|
- Input:
|
||||||
|
- `url` (string, required): URL of the website to fetch
|
||||||
|
- `headers` (object, optional): Custom headers to include in the request
|
||||||
|
- Returns the raw HTML content of the webpage
|
||||||
|
|
||||||
|
- **fetch_json**
|
||||||
|
- Fetch a JSON file from a URL
|
||||||
|
- Input:
|
||||||
|
- `url` (string, required): URL of the JSON to fetch
|
||||||
|
- `headers` (object, optional): Custom headers to include in the request
|
||||||
|
- Returns the parsed JSON content
|
||||||
|
|
||||||
|
- **fetch_txt**
|
||||||
|
- Fetch a website and return the content as plain text (no HTML)
|
||||||
|
- Input:
|
||||||
|
- `url` (string, required): URL of the website to fetch
|
||||||
|
- `headers` (object, optional): Custom headers to include in the request
|
||||||
|
- Returns the text content of the webpage with HTML tags, scripts, and styles removed
|
||||||
|
|
||||||
|
- **fetch_markdown**
|
||||||
|
- Fetch a website and return the content as Markdown
|
||||||
|
- Input:
|
||||||
|
- `url` (string, required): URL of the website to fetch
|
||||||
|
- `headers` (object, optional): Custom headers to include in the request
|
||||||
|
- Returns the content of the webpage converted to Markdown format
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
|
||||||
|
This server does not provide any persistent resources. It's designed to fetch and transform web content on demand.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Install dependencies: `npm install`
|
||||||
|
3. Build the server: `npm run build`
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
To use the server, you can run it directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start the Fetch MCP Server running on stdio.
|
||||||
|
|
||||||
|
### Usage with Desktop App
|
||||||
|
|
||||||
|
To integrate this server with a desktop app, add the following to your app's server configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"fetch": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"{ABSOLUTE PATH TO FILE HERE}/dist/index.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Fetches web content using modern fetch API
|
||||||
|
- Supports custom headers for requests
|
||||||
|
- Provides content in multiple formats: HTML, JSON, plain text, and Markdown
|
||||||
|
- Uses JSDOM for HTML parsing and text extraction
|
||||||
|
- Uses TurndownService for HTML to Markdown conversion
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
- Run `npm run dev` to start the TypeScript compiler in watch mode
|
||||||
|
- Use `npm test` to run the test suite
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License.
|
4
jest.config.js
Normal file
4
jest.config.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default {
|
||||||
|
preset: "ts-jest",
|
||||||
|
testEnvironment: "node",
|
||||||
|
};
|
32
package.json
Normal file
32
package.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "fetch",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "zcaceres (@zachcaceres zach.dev)",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && shx chmod +x dist/*.js",
|
||||||
|
"prepare": "npm run build",
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
|
"turndown": "^7.2.0",
|
||||||
|
"zod": "^3.24.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/jsdom": "^21.1.7",
|
||||||
|
"@types/node": "^22.10.2",
|
||||||
|
"@types/turndown": "^5.0.5",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
3124
pnpm-lock.yaml
generated
Normal file
3124
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
208
src/Fetcher.test.ts
Normal file
208
src/Fetcher.test.ts
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import { Fetcher } from "./Fetcher";
|
||||||
|
import { JSDOM } from "jsdom";
|
||||||
|
import TurndownService from "turndown";
|
||||||
|
|
||||||
|
global.fetch = jest.fn();
|
||||||
|
|
||||||
|
jest.mock("jsdom");
|
||||||
|
|
||||||
|
jest.mock("turndown");
|
||||||
|
|
||||||
|
describe("Fetcher", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockRequest = {
|
||||||
|
url: "https://example.com",
|
||||||
|
headers: { "Custom-Header": "Value" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockHtml = `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Test Page</title>
|
||||||
|
<script>console.log('This should be removed');</script>
|
||||||
|
<style>body { color: red; }</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
<p>This is a test paragraph.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe("html", () => {
|
||||||
|
it("should return the raw HTML content", async () => {
|
||||||
|
(fetch as jest.Mock).mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
text: jest.fn().mockResolvedValueOnce(mockHtml),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await Fetcher.html(mockRequest);
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: [{ type: "text", text: mockHtml }],
|
||||||
|
isError: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle errors", async () => {
|
||||||
|
(fetch as jest.Mock).mockRejectedValueOnce(new Error("Network error"));
|
||||||
|
|
||||||
|
const result = await Fetcher.html(mockRequest);
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Failed to fetch https://example.com: Network error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("json", () => {
|
||||||
|
it("should parse and return JSON content", async () => {
|
||||||
|
const mockJson = { key: "value" };
|
||||||
|
(fetch as jest.Mock).mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: jest.fn().mockResolvedValueOnce(mockJson),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await Fetcher.json(mockRequest);
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: [{ type: "text", text: JSON.stringify(mockJson) }],
|
||||||
|
isError: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle errors", async () => {
|
||||||
|
(fetch as jest.Mock).mockRejectedValueOnce(new Error("Invalid JSON"));
|
||||||
|
|
||||||
|
const result = await Fetcher.json(mockRequest);
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Failed to fetch https://example.com: Invalid JSON",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("txt", () => {
|
||||||
|
it("should return plain text content without HTML tags, scripts, and styles", async () => {
|
||||||
|
(fetch as jest.Mock).mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
text: jest.fn().mockResolvedValueOnce(mockHtml),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockTextContent = "Hello World This is a test paragraph.";
|
||||||
|
// @ts-expect-error Mocking JSDOM
|
||||||
|
(JSDOM as jest.Mock).mockImplementationOnce(() => ({
|
||||||
|
window: {
|
||||||
|
document: {
|
||||||
|
body: {
|
||||||
|
textContent: mockTextContent,
|
||||||
|
},
|
||||||
|
getElementsByTagName: jest.fn().mockReturnValue([]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await Fetcher.txt(mockRequest);
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: [{ type: "text", text: mockTextContent }],
|
||||||
|
isError: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle errors", async () => {
|
||||||
|
(fetch as jest.Mock).mockRejectedValueOnce(new Error("Parsing error"));
|
||||||
|
|
||||||
|
const result = await Fetcher.txt(mockRequest);
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Failed to fetch https://example.com: Parsing error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("markdown", () => {
|
||||||
|
it("should convert HTML to markdown", async () => {
|
||||||
|
(fetch as jest.Mock).mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
text: jest.fn().mockResolvedValueOnce(mockHtml),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockMarkdown = "# Hello World\n\nThis is a test paragraph.";
|
||||||
|
(TurndownService as jest.Mock).mockImplementationOnce(() => ({
|
||||||
|
turndown: jest.fn().mockReturnValueOnce(mockMarkdown),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await Fetcher.markdown(mockRequest);
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: [{ type: "text", text: mockMarkdown }],
|
||||||
|
isError: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle errors", async () => {
|
||||||
|
(fetch as jest.Mock).mockRejectedValueOnce(new Error("Conversion error"));
|
||||||
|
|
||||||
|
const result = await Fetcher.markdown(mockRequest);
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Failed to fetch https://example.com: Conversion error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("error handling", () => {
|
||||||
|
it("should handle non-OK responses", async () => {
|
||||||
|
(fetch as jest.Mock).mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await Fetcher.html(mockRequest);
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Failed to fetch https://example.com: HTTP error: 404",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle unknown errors", async () => {
|
||||||
|
(fetch as jest.Mock).mockRejectedValueOnce("Unknown error");
|
||||||
|
|
||||||
|
const result = await Fetcher.html(mockRequest);
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Failed to fetch https://example.com: Unknown error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
104
src/Fetcher.ts
Normal file
104
src/Fetcher.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { JSDOM } from "jsdom";
|
||||||
|
import TurndownService from "turndown";
|
||||||
|
import { RequestPayload } from "./types.js";
|
||||||
|
|
||||||
|
export class Fetcher {
|
||||||
|
private static async _fetch({
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
}: RequestPayload): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
throw new Error(`Failed to fetch ${url}: ${e.message}`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Failed to fetch ${url}: Unknown error`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async html(requestPayload: RequestPayload) {
|
||||||
|
try {
|
||||||
|
const response = await this._fetch(requestPayload);
|
||||||
|
const html = await response.text();
|
||||||
|
return { content: [{ type: "text", text: html }], isError: false };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: (error as Error).message }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async json(requestPayload: RequestPayload) {
|
||||||
|
try {
|
||||||
|
const response = await this._fetch(requestPayload);
|
||||||
|
const json = await response.json();
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(json) }],
|
||||||
|
isError: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: (error as Error).message }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async txt(requestPayload: RequestPayload) {
|
||||||
|
try {
|
||||||
|
const response = await this._fetch(requestPayload);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const dom = new JSDOM(html);
|
||||||
|
const document = dom.window.document;
|
||||||
|
|
||||||
|
const scripts = document.getElementsByTagName("script");
|
||||||
|
const styles = document.getElementsByTagName("style");
|
||||||
|
Array.from(scripts).forEach((script) => script.remove());
|
||||||
|
Array.from(styles).forEach((style) => style.remove());
|
||||||
|
|
||||||
|
const text = document.body.textContent || "";
|
||||||
|
|
||||||
|
const normalizedText = text.replace(/\s+/g, " ").trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: normalizedText }],
|
||||||
|
isError: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: (error as Error).message }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async markdown(requestPayload: RequestPayload) {
|
||||||
|
try {
|
||||||
|
const response = await this._fetch(requestPayload);
|
||||||
|
const html = await response.text();
|
||||||
|
const turndownService = new TurndownService();
|
||||||
|
const markdown = turndownService.turndown(html);
|
||||||
|
return { content: [{ type: "text", text: markdown }], isError: false };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: (error as Error).message }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
138
src/index.ts
Normal file
138
src/index.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import { RequestPayloadSchema } from "./types.js";
|
||||||
|
import { Fetcher } from "./Fetcher.js";
|
||||||
|
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: "zcaceres/fetch",
|
||||||
|
version: "0.1.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
resources: {},
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: "fetch_html",
|
||||||
|
description: "Fetch a website and return the content as HTML",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: "string",
|
||||||
|
description: "URL of the website to fetch",
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: "object",
|
||||||
|
description: "Optional headers to include in the request",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["url"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fetch_markdown",
|
||||||
|
description: "Fetch a website and return the content as Markdown",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: "string",
|
||||||
|
description: "URL of the website to fetch",
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: "object",
|
||||||
|
description: "Optional headers to include in the request",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["url"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fetch_txt",
|
||||||
|
description:
|
||||||
|
"Fetch a website, return the content as plain text (no HTML)",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: "string",
|
||||||
|
description: "URL of the website to fetch",
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: "object",
|
||||||
|
description: "Optional headers to include in the request",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["url"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fetch_json",
|
||||||
|
description: "Fetch a JSON file from a URL",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: "string",
|
||||||
|
description: "URL of the JSON to fetch",
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
type: "object",
|
||||||
|
description: "Optional headers to include in the request",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["url"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
|
const validatedArgs = RequestPayloadSchema.parse(args);
|
||||||
|
|
||||||
|
if (request.params.name === "fetch_html") {
|
||||||
|
const fetchResult = await Fetcher.html(validatedArgs);
|
||||||
|
return fetchResult;
|
||||||
|
}
|
||||||
|
if (request.params.name === "fetch_json") {
|
||||||
|
const fetchResult = await Fetcher.json(validatedArgs);
|
||||||
|
return fetchResult;
|
||||||
|
}
|
||||||
|
if (request.params.name === "fetch_txt") {
|
||||||
|
const fetchResult = await Fetcher.txt(validatedArgs);
|
||||||
|
return fetchResult;
|
||||||
|
}
|
||||||
|
if (request.params.name === "fetch_markdown") {
|
||||||
|
const fetchResult = await Fetcher.markdown(validatedArgs);
|
||||||
|
return fetchResult;
|
||||||
|
}
|
||||||
|
throw new Error("Tool not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
console.log("Fetch MCP Server running on stdio");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("Fatal error in main():", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
8
src/types.ts
Normal file
8
src/types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const RequestPayloadSchema = z.object({
|
||||||
|
url: z.string().url(),
|
||||||
|
headers: z.record(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RequestPayload = z.infer<typeof RequestPayloadSchema>;
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "node16",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
|
||||||
|
}
|
Reference in New Issue
Block a user