From c16b8bea31948df5ad52a96f1d2a367808ecac88 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Tue, 27 May 2025 18:31:19 +0000 Subject: [PATCH 01/38] docs(examples): Add example environment configuration and README files for SDK usage Signed-off-by: Eden Reich --- README.md | 10 +++++++-- examples/.env.example | 48 ++++++++++++++++++++++++++++++++++++++++ examples/README.md | 19 ++++++++++++++++ examples/basic/README.md | 0 examples/chat/README.md | 0 examples/mcp/README.md | 0 6 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 examples/.env.example create mode 100644 examples/README.md create mode 100644 examples/basic/README.md create mode 100644 examples/chat/README.md create mode 100644 examples/mcp/README.md diff --git a/README.md b/README.md index 20c6a56..f82378c 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,14 @@ An SDK written in TypeScript for the [Inference Gateway](https://github.com/eden - [Usage](#usage) - [Creating a Client](#creating-a-client) - [Listing Models](#listing-models) + - [Listing MCP Tools](#listing-mcp-tools) - [Creating Chat Completions](#creating-chat-completions) - [Streaming Chat Completions](#streaming-chat-completions) - [Tool Calls](#tool-calls) - [Proxying Requests](#proxying-requests) - [Health Check](#health-check) - [Creating a Client with Custom Options](#creating-a-client-with-custom-options) + - [Examples](#examples) - [Contributing](#contributing) - [License](#license) @@ -235,7 +237,7 @@ To proxy requests directly to a provider: import { InferenceGatewayClient, Provider } from '@inference-gateway/sdk'; const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', + baseURL: 'http://localhost:8080', }); try { @@ -261,7 +263,7 @@ To check if the Inference Gateway is running: import { InferenceGatewayClient } from '@inference-gateway/sdk'; const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', + baseURL: 'http://localhost:8080', }); try { @@ -292,6 +294,10 @@ const clientWithHeaders = client.withOptions({ }); ``` +### Examples + +For more examples, check the [examples directory](./examples). + ## Contributing Please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file for information about how to get involved. We welcome issues, questions, and pull requests. diff --git a/examples/.env.example b/examples/.env.example new file mode 100644 index 0000000..ae6a429 --- /dev/null +++ b/examples/.env.example @@ -0,0 +1,48 @@ + +# General settings +ENVIRONMENT=development +ENABLE_TELEMETRY=false +ENABLE_AUTH=false +# Model Context Protocol (MCP) +MCP_ENABLE=false +MCP_EXPOSE=false +MCP_SERVERS= +MCP_CLIENT_TIMEOUT=5s +MCP_DIAL_TIMEOUT=3s +MCP_TLS_HANDSHAKE_TIMEOUT=3s +MCP_RESPONSE_HEADER_TIMEOUT=3s +MCP_EXPECT_CONTINUE_TIMEOUT=1s +MCP_REQUEST_TIMEOUT=5s +# OpenID Connect +OIDC_ISSUER_URL=http://keycloak:8080/realms/inference-gateway-realm +OIDC_CLIENT_ID=inference-gateway-client +OIDC_CLIENT_SECRET= +# Server settings +SERVER_HOST=0.0.0.0 +SERVER_PORT=8080 +SERVER_READ_TIMEOUT=30s +SERVER_WRITE_TIMEOUT=30s +SERVER_IDLE_TIMEOUT=120s +SERVER_TLS_CERT_PATH= +SERVER_TLS_KEY_PATH= +# Client settings +CLIENT_TIMEOUT=30s +CLIENT_MAX_IDLE_CONNS=20 +CLIENT_MAX_IDLE_CONNS_PER_HOST=20 +CLIENT_IDLE_CONN_TIMEOUT=30s +CLIENT_TLS_MIN_VERSION=TLS12 +# Providers +ANTHROPIC_API_URL=https://api.anthropic.com/v1 +ANTHROPIC_API_KEY= +CLOUDFLARE_API_URL=https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai +CLOUDFLARE_API_KEY= +COHERE_API_URL=https://api.cohere.ai +COHERE_API_KEY= +GROQ_API_URL=https://api.groq.com/openai/v1 +GROQ_API_KEY= +OLLAMA_API_URL=http://ollama:8080/v1 +OLLAMA_API_KEY= +OPENAI_API_URL=https://api.openai.com/v1 +OPENAI_API_KEY= +DEEPSEEK_API_URL=https://api.deepseek.com +DEEPSEEK_API_KEY= diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..ec564b5 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,19 @@ +# Examples + +This directory contains examples that demonstrate how to use the Typescript SDK. + +## Quick Start + +1. Copy the `.env.example` file to `.env` and fill in your API key. + +2. Start the Inference Gateway locally: + + ```bash + docker run -p 8080:8080 --env-file .env ghcr.io/inference-gateway/inference-gateway:latest + ``` + +3. Review the different examples in the specific directories: + + - [Basic](./basic): A basic example of how to use the SDK. + - [Chat](./chat): An example of how to use the SDK for chat applications. + - [MCP](./mcp): An example of how to use the SDK with the MCP in a Multi Provider architecture. diff --git a/examples/basic/README.md b/examples/basic/README.md new file mode 100644 index 0000000..e69de29 diff --git a/examples/chat/README.md b/examples/chat/README.md new file mode 100644 index 0000000..e69de29 diff --git a/examples/mcp/README.md b/examples/mcp/README.md new file mode 100644 index 0000000..e69de29 From 52954c1d6d999cd212fa5540a8331b898a5e064c Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Tue, 27 May 2025 18:33:07 +0000 Subject: [PATCH 02/38] docs: Update README to include pre-requisites for using the Typescript SDK examples Signed-off-by: Eden Reich --- examples/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/README.md b/examples/README.md index ec564b5..7b57848 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,6 +2,10 @@ This directory contains examples that demonstrate how to use the Typescript SDK. +## Pre-requisites + +You should have docker installed or use the dev container in VS Code which has all the tools you might need. + ## Quick Start 1. Copy the `.env.example` file to `.env` and fill in your API key. From 9b2eb7aa24efa6801e7646e0c30b90a1d138fc7f Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Tue, 27 May 2025 18:34:10 +0000 Subject: [PATCH 03/38] docs(examples): Update example links in README and add list example README Signed-off-by: Eden Reich --- examples/README.md | 2 +- examples/{basic => list}/README.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename examples/{basic => list}/README.md (100%) diff --git a/examples/README.md b/examples/README.md index 7b57848..64469f4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,6 +18,6 @@ You should have docker installed or use the dev container in VS Code which has a 3. Review the different examples in the specific directories: - - [Basic](./basic): A basic example of how to use the SDK. + - [List](./list): An example of how to use the SDK to list models and MCP tools. - [Chat](./chat): An example of how to use the SDK for chat applications. - [MCP](./mcp): An example of how to use the SDK with the MCP in a Multi Provider architecture. diff --git a/examples/basic/README.md b/examples/list/README.md similarity index 100% rename from examples/basic/README.md rename to examples/list/README.md From 6550c8553fad31a3feb9e18d1eacbb5a503ca933 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Tue, 27 May 2025 19:03:09 +0000 Subject: [PATCH 04/38] docs(examples): Add examples for chat, list, and MCP using Inference Gateway SDK - Created a chat example with package.json, tsconfig.json, and index.ts to demonstrate chat completions and streaming responses. - Added a list example with package.json, tsconfig.json, and index.ts for listing models and MCP tools. - Included README.md files for both chat and list examples with instructions for getting started. - Added .gitignore files to exclude node_modules in both list and MCP examples. Signed-off-by: Eden Reich --- .gitignore | 1 + README.md | 2 +- examples/README.md | 11 +- examples/chat/.gitignore | 1 + examples/chat/README.md | 19 ++ examples/chat/index.ts | 0 examples/chat/package-lock.json | 561 ++++++++++++++++++++++++++++++++ examples/chat/package.json | 19 ++ examples/chat/tsconfig.json | 12 + examples/list/.gitignore | 1 + examples/list/README.md | 19 ++ examples/list/index.ts | 21 ++ examples/list/package-lock.json | 561 ++++++++++++++++++++++++++++++++ examples/list/package.json | 19 ++ examples/list/tsconfig.json | 12 + examples/mcp/.gitignore | 1 + examples/mcp/README.md | 7 + 17 files changed, 1264 insertions(+), 3 deletions(-) create mode 100644 examples/chat/.gitignore create mode 100644 examples/chat/index.ts create mode 100644 examples/chat/package-lock.json create mode 100644 examples/chat/package.json create mode 100644 examples/chat/tsconfig.json create mode 100644 examples/list/.gitignore create mode 100644 examples/list/index.ts create mode 100644 examples/list/package-lock.json create mode 100644 examples/list/package.json create mode 100644 examples/list/tsconfig.json create mode 100644 examples/mcp/.gitignore diff --git a/.gitignore b/.gitignore index 0e75fe5..3176671 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules dist coverage +**/.env diff --git a/README.md b/README.md index f82378c..8195432 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ try { console.log('All models:', models); // List models from a specific provider - const openaiModels = await client.listModels(Provider.OpenAI); + const openaiModels = await client.listModels(Provider.openai); console.log('OpenAI models:', openaiModels); } catch (error) { console.error('Error:', error); diff --git a/examples/README.md b/examples/README.md index 64469f4..c556ebd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,10 +13,17 @@ You should have docker installed or use the dev container in VS Code which has a 2. Start the Inference Gateway locally: ```bash - docker run -p 8080:8080 --env-file .env ghcr.io/inference-gateway/inference-gateway:latest + docker run --rm -p 8080:8080 --env-file .env ghcr.io/inference-gateway/inference-gateway:latest ``` -3. Review the different examples in the specific directories: +3. On another terminal export the provider and the LLM you intent to use: + + ```bash + export PROVIDER=groq + export LLM=groq/meta-llama/llama-4-maverick-17b-128e-instruct + ``` + +4. Review the different examples in the specific directories: - [List](./list): An example of how to use the SDK to list models and MCP tools. - [Chat](./chat): An example of how to use the SDK for chat applications. diff --git a/examples/chat/.gitignore b/examples/chat/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/examples/chat/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/chat/README.md b/examples/chat/README.md index e69de29..d6435ce 100644 --- a/examples/chat/README.md +++ b/examples/chat/README.md @@ -0,0 +1,19 @@ +# Chat Example + +This example demonstrates how to use the Inference Gateway SDK for chat applications. It includes creating chat completions and streaming responses using the Typescript SDK. + +## Getting Started + +1. Ensure you have the Inference Gateway running locally or have access to an instance, if not please read the [Quick Start](../README.md#quick-start) section in the main README. + +2. Install the SDK if you haven't already: + + ```bash + npm install + ``` + +3. Run the example: + + ```bash + npm start + ``` diff --git a/examples/chat/index.ts b/examples/chat/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/examples/chat/package-lock.json b/examples/chat/package-lock.json new file mode 100644 index 0000000..baa6229 --- /dev/null +++ b/examples/chat/package-lock.json @@ -0,0 +1,561 @@ +{ + "name": "chat", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chat", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@inference-gateway/sdk": "^0.7.1" + }, + "devDependencies": { + "tsx": "^4.19.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inference-gateway/sdk": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.1.tgz", + "integrity": "sha512-O6wHlmB5XmQApASaw6yhTaRHMFkSzLUl9DNGb2RYN3/0wK5Bdlymed8HCl69dbATfkEh3eXU9SiZ8FG/pww7Lg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0", + "npm": ">=10.9.0" + }, + "peerDependencies": { + "node-fetch": "^2.7.0" + }, + "peerDependenciesMeta": { + "node-fetch": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + } + } +} diff --git a/examples/chat/package.json b/examples/chat/package.json new file mode 100644 index 0000000..bf7359b --- /dev/null +++ b/examples/chat/package.json @@ -0,0 +1,19 @@ +{ + "name": "chat", + "version": "1.0.0", + "description": "This example demonstrates how to use the Inference Gateway SDK for chat applications. It includes creating chat completions and streaming responses using the Typescript SDK.", + "main": "index.js", + "private": true, + "scripts": { + "start": "tsx index.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@inference-gateway/sdk": "^0.7.1" + }, + "devDependencies": { + "tsx": "^4.19.4" + } +} diff --git a/examples/chat/tsconfig.json b/examples/chat/tsconfig.json new file mode 100644 index 0000000..3a2170b --- /dev/null +++ b/examples/chat/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["index.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/list/.gitignore b/examples/list/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/examples/list/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/list/README.md b/examples/list/README.md index e69de29..4f91a66 100644 --- a/examples/list/README.md +++ b/examples/list/README.md @@ -0,0 +1,19 @@ +# List Example + +This example demonstrates how to use the Inference Gateway SDK for listing models and MCP tools. It includes making requests to the SDK and handling responses using the Typescript SDK. + +## Getting Started + +1. Ensure you have the Inference Gateway running locally or have access to an instance, if not please read the [Quick Start](../README.md#quick-start) section in the main README. + +2. Install the SDK if you haven't already: + + ```bash + npm install + ``` + +3. Run the example: + + ```bash + npm start + ``` diff --git a/examples/list/index.ts b/examples/list/index.ts new file mode 100644 index 0000000..d758dbb --- /dev/null +++ b/examples/list/index.ts @@ -0,0 +1,21 @@ +import { InferenceGatewayClient, Provider } from '@inference-gateway/sdk'; + +(async () => { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + const provider = process.env.PROVIDER as Provider; + + try { + // List all models + const models = await client.listModels(); + console.log('All models:', models); + + // List models from a specific provider + const llms = await client.listModels(provider); + console.log(`Specific ${provider} models:`, llms); + } catch (error) { + console.error('Error:', error); + } +})(); diff --git a/examples/list/package-lock.json b/examples/list/package-lock.json new file mode 100644 index 0000000..0e42f8a --- /dev/null +++ b/examples/list/package-lock.json @@ -0,0 +1,561 @@ +{ + "name": "list", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "list", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@inference-gateway/sdk": "^0.7.1" + }, + "devDependencies": { + "tsx": "^4.19.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inference-gateway/sdk": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.1.tgz", + "integrity": "sha512-O6wHlmB5XmQApASaw6yhTaRHMFkSzLUl9DNGb2RYN3/0wK5Bdlymed8HCl69dbATfkEh3eXU9SiZ8FG/pww7Lg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0", + "npm": ">=10.9.0" + }, + "peerDependencies": { + "node-fetch": "^2.7.0" + }, + "peerDependenciesMeta": { + "node-fetch": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + } + } +} diff --git a/examples/list/package.json b/examples/list/package.json new file mode 100644 index 0000000..98f5440 --- /dev/null +++ b/examples/list/package.json @@ -0,0 +1,19 @@ +{ + "name": "list", + "version": "1.0.0", + "description": "This example demonstrates how to use the Inference Gateway SDK for listing models and MCP tools. It includes making requests to the SDK and handling responses using the Typescript SDK.", + "main": "index.js", + "private": true, + "scripts": { + "start": "tsx index.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@inference-gateway/sdk": "^0.7.1" + }, + "devDependencies": { + "tsx": "^4.19.4" + } +} diff --git a/examples/list/tsconfig.json b/examples/list/tsconfig.json new file mode 100644 index 0000000..3a2170b --- /dev/null +++ b/examples/list/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["index.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/mcp/.gitignore b/examples/mcp/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/examples/mcp/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/mcp/README.md b/examples/mcp/README.md index e69de29..5949837 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -0,0 +1,7 @@ +# MCP Example + +This example demonstrates how to use the Inference Gateway SDK for listing models and MCP tools. It includes making requests to the SDK and handling responses using the Typescript SDK. + +## Getting Started + +1. For this example you should have docker compose installed, if you're using the dev container in VS Code you should be all set. From e76577862ad8aeeabad170a180b4907d7962b3ab Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Tue, 27 May 2025 20:14:59 +0000 Subject: [PATCH 05/38] docs(exmaples): Add sample files and configuration for MCP example - Created a new directory with sample files for the MCP filesystem server, including `sample.txt`, `data.json`, and `config.yaml`. - Added a README.md file to describe the purpose and usage of the sample data. - Implemented a TypeScript configuration file (`tsconfig.json`) for the MCP example. - Created a package.json and package-lock.json for managing dependencies in the examples directory. - Updated the OpenAPI specification to include new endpoints and improved formatting for consistency. Signed-off-by: Eden Reich --- eslint.config.mjs | 81 +++- examples/QUICKSTART.md | 122 ++++++ examples/README.md | 82 +++- examples/chat/README.md | 55 ++- examples/chat/index.ts | 225 +++++++++++ examples/list/README.md | 43 +- examples/list/index.ts | 125 +++++- examples/mcp/README.md | 284 ++++++++++++- examples/mcp/docker-compose.yml | 213 ++++++++++ examples/mcp/index.ts | 255 ++++++++++++ examples/mcp/package-lock.json | 561 ++++++++++++++++++++++++++ examples/mcp/package.json | 22 + examples/mcp/shared/README.md | 13 + examples/mcp/shared/config.yaml | 49 +++ examples/mcp/shared/data.json | 42 ++ examples/mcp/shared/sample.txt | 18 + examples/mcp/tsconfig.json | 12 + examples/package-lock.json | 13 + examples/package.json | 25 ++ openapi.yaml | 690 ++++++++++++++++---------------- 20 files changed, 2561 insertions(+), 369 deletions(-) create mode 100644 examples/QUICKSTART.md create mode 100644 examples/mcp/docker-compose.yml create mode 100644 examples/mcp/index.ts create mode 100644 examples/mcp/package-lock.json create mode 100644 examples/mcp/package.json create mode 100644 examples/mcp/shared/README.md create mode 100644 examples/mcp/shared/config.yaml create mode 100644 examples/mcp/shared/data.json create mode 100644 examples/mcp/shared/sample.txt create mode 100644 examples/mcp/tsconfig.json create mode 100644 examples/package-lock.json create mode 100644 examples/package.json diff --git a/eslint.config.mjs b/eslint.config.mjs index 6aff49f..2bc7eae 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,13 +10,26 @@ export default [ plugins: { prettier: prettier, }, + languageOptions: { + globals: { + console: 'readonly', + process: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + global: 'readonly', + module: 'readonly', + require: 'readonly', + exports: 'readonly', + }, + }, rules: { 'prettier/prettier': 'error', ...eslint.configs.recommended.rules, }, }, { - files: ['**/*.ts'], + files: ['src/**/*.ts', 'tests/**/*.ts'], languageOptions: { parser: tsParser, parserOptions: { @@ -25,12 +38,28 @@ export default [ sourceType: 'module', }, globals: { - // Add Jest globals + console: 'readonly', + process: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + global: 'readonly', + module: 'readonly', + require: 'readonly', + exports: 'readonly', describe: 'readonly', it: 'readonly', expect: 'readonly', beforeEach: 'readonly', + afterEach: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly', jest: 'readonly', + Headers: 'readonly', + fetch: 'readonly', + ReadableStream: 'readonly', + Response: 'readonly', + Request: 'readonly', }, }, plugins: { @@ -45,10 +74,54 @@ export default [ ...tseslint.configs.recommended.rules, }, }, + { + files: ['examples/**/*.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + globals: { + console: 'readonly', + process: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + global: 'readonly', + module: 'readonly', + require: 'readonly', + exports: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' }, + ], + }, + }, { files: ['**/*.test.ts'], - env: { - 'jest/globals': true, + languageOptions: { + globals: { + describe: 'readonly', + it: 'readonly', + expect: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly', + jest: 'readonly', + Headers: 'readonly', + fetch: 'readonly', + ReadableStream: 'readonly', + Response: 'readonly', + Request: 'readonly', + }, }, }, ]; diff --git a/examples/QUICKSTART.md b/examples/QUICKSTART.md new file mode 100644 index 0000000..d72bffb --- /dev/null +++ b/examples/QUICKSTART.md @@ -0,0 +1,122 @@ +# Quick Start Guide + +This guide will help you run all TypeScript SDK examples quickly. + +## Prerequisites + +1. **Docker** - For running the Inference Gateway +2. **Node.js** - For running the examples +3. **API Key** - For at least one AI provider + +## 1. Setup Environment + +1. Copy the environment template: + + ```bash + cp .env.example .env + ``` + +2. Add your API keys to `.env`: + ```bash + # Choose one or more providers + GROQ_API_KEY=your_groq_key_here + OPENAI_API_KEY=your_openai_key_here + ANTHROPIC_API_KEY=your_anthropic_key_here + ``` + +## 2. Start Inference Gateway + +Choose one of these options: + +### Option A: Basic Gateway (for List and Chat examples) + +```bash +docker run --rm -p 8080:8080 --env-file .env ghcr.io/inference-gateway/inference-gateway:latest +``` + +### Option B: Gateway with MCP (for all examples) + +```bash +cd mcp +npm run compose:up +``` + +## 3. Test the Examples + +### Quick Test - List Models + +```bash +cd list +npm install +npm start +``` + +### Chat Example + +```bash +cd chat +export PROVIDER=groq +export LLM=groq/meta-llama/llama-3.3-70b-versatile +npm install +npm start +``` + +### MCP Example (requires Docker Compose setup) + +```bash +cd mcp +export PROVIDER=groq +export LLM=groq/meta-llama/llama-3.3-70b-versatile +npm install +npm start +``` + +## 4. Popular Provider/Model Combinations + +### Groq (Fast inference) + +```bash +export PROVIDER=groq +export LLM=groq/meta-llama/llama-3.3-70b-versatile +``` + +### OpenAI (High quality) + +```bash +export PROVIDER=openai +export LLM=gpt-4o +``` + +### Anthropic (Strong reasoning) + +```bash +export PROVIDER=anthropic +export LLM=claude-3-5-sonnet-20241022 +``` + +## Troubleshooting + +### Gateway not responding + +- Check if Docker container is running: `docker ps` +- Test health: `curl http://localhost:8080/health` +- Check logs: `docker logs ` + +### Authentication errors + +- Verify API key is correct in `.env` +- Ensure the key has sufficient permissions +- Try a different provider + +### Model not found + +- Use the list example to see available models +- Check if the model name is correct +- Try a different model from the same provider + +## Next Steps + +1. Explore each example in detail +2. Modify the examples for your use case +3. Build your own applications using the patterns shown +4. Check the main [README](../README.md) for more advanced usage diff --git a/examples/README.md b/examples/README.md index c556ebd..db4b598 100644 --- a/examples/README.md +++ b/examples/README.md @@ -25,6 +25,82 @@ You should have docker installed or use the dev container in VS Code which has a 4. Review the different examples in the specific directories: - - [List](./list): An example of how to use the SDK to list models and MCP tools. - - [Chat](./chat): An example of how to use the SDK for chat applications. - - [MCP](./mcp): An example of how to use the SDK with the MCP in a Multi Provider architecture. + - [List](./list): Demonstrates listing models, MCP tools, health checks, and provider proxy functionality. + - [Chat](./chat): Shows chat completions, streaming responses, multi-turn conversations, and function calling. + - [MCP](./mcp): Illustrates Model Context Protocol integration with file operations, web scraping, and multi-tool conversations using Docker Compose. + +## Examples Overview + +### [List Example](./list) + +**Purpose**: Explore available models and MCP tools across providers + +**Features**: + +- List all available models across providers +- Filter models by specific provider +- Discover MCP tools and their schemas +- Health check validation +- Direct provider API access via proxy + +**Best for**: Understanding what's available in your Inference Gateway setup + +### [Chat Example](./chat) + +**Purpose**: Demonstrate various chat completion patterns + +**Features**: + +- Simple request/response chat completions +- Real-time streaming responses +- Multi-turn conversation handling +- Function/tool calling with AI models +- Temperature comparison examples + +**Best for**: Building chat applications and understanding different interaction patterns + +### [MCP Example](./mcp) + +**Purpose**: Showcase Model Context Protocol integration + +**Features**: + +- Docker Compose orchestration of MCP servers +- File system operations via MCP tools +- Web content fetching and search simulation +- Multi-tool conversations +- Real-time tool execution streaming + +**Best for**: Integrating external tools and services with AI models + +## Running Examples + +Each example can be run independently: + +```bash +# Navigate to any example directory +cd list # or chat, or mcp + +# Install dependencies +npm install + +# Run the example +npm start +``` + +## Environment Variables + +All examples support these environment variables: + +- `PROVIDER` - AI provider to use (groq, openai, anthropic, etc.) +- `LLM` - Specific model to use (e.g., groq/meta-llama/llama-3.3-70b-versatile) + +Provider-specific API keys should be set in the `.env` file (see `.env.example`). + +## Example Combinations + +You can combine concepts from different examples: + +1. **List + Chat**: Discover available models, then use them for chat +2. **Chat + MCP**: Use function calling with MCP tools for enhanced capabilities +3. **List + MCP**: Explore MCP tools, then integrate them into conversations diff --git a/examples/chat/README.md b/examples/chat/README.md index d6435ce..e28c3bf 100644 --- a/examples/chat/README.md +++ b/examples/chat/README.md @@ -1,10 +1,17 @@ # Chat Example -This example demonstrates how to use the Inference Gateway SDK for chat applications. It includes creating chat completions and streaming responses using the Typescript SDK. +This example demonstrates how to use the Inference Gateway SDK for chat applications. It includes creating chat completions, streaming responses, multi-turn conversations, and function calling using the TypeScript SDK. + +## Features Demonstrated + +1. **Simple Chat Completion** - Basic request/response chat +2. **Streaming Chat Completion** - Real-time streaming responses +3. **Multi-turn Conversation** - Maintaining conversation context +4. **Function Calling** - Using tools/functions with models ## Getting Started -1. Ensure you have the Inference Gateway running locally or have access to an instance, if not please read the [Quick Start](../README.md#quick-start) section in the main README. +1. Ensure you have the Inference Gateway running locally or have access to an instance. If not, please read the [Quick Start](../README.md#quick-start) section in the main README. 2. Install the SDK if you haven't already: @@ -12,8 +19,50 @@ This example demonstrates how to use the Inference Gateway SDK for chat applicat npm install ``` -3. Run the example: +3. Set the required environment variables: + + ```bash + export PROVIDER=groq + export LLM=groq/meta-llama/llama-3.3-70b-versatile + ``` + + Or for OpenAI: + + ```bash + export PROVIDER=openai + export LLM=gpt-4o + ``` + +4. Run the example: ```bash npm start ``` + +## Example Output + +The example will demonstrate: + +- A simple chat completion about TypeScript +- A streaming story about a robot learning to paint +- A multi-turn conversation about JavaScript programming +- Function calling for weather information (simulated) + +## Supported Providers + +This example works with any provider supported by the Inference Gateway: + +- `openai` - OpenAI models (GPT-4, GPT-3.5, etc.) +- `groq` - Groq's fast inference models +- `anthropic` - Claude models +- `ollama` - Local models via Ollama +- `cohere` - Cohere models +- `deepseek` - DeepSeek models +- `cloudflare` - Cloudflare Workers AI + +## Notes + +- The function calling example simulates weather API calls - in a real application, you would implement actual function execution +- Streaming responses provide real-time output, perfect for interactive applications +- Multi-turn conversations maintain context across multiple exchanges +- Temperature and max_tokens parameters can be adjusted for different use cases diff --git a/examples/chat/index.ts b/examples/chat/index.ts index e69de29..1c70743 100644 --- a/examples/chat/index.ts +++ b/examples/chat/index.ts @@ -0,0 +1,225 @@ +import { + ChatCompletionToolType, + InferenceGatewayClient, + MessageRole, + Provider, +} from '@inference-gateway/sdk'; + +const main = async () => { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + const provider = process.env.PROVIDER as Provider; + const model = process.env.LLM; + + if (!provider) { + console.error('Please set the PROVIDER environment variable'); + process.exit(1); + } + + if (!model) { + console.error('Please set the LLM environment variable'); + process.exit(1); + } + + console.log(`Using provider: ${provider}`); + console.log(`Using model: ${model}`); + console.log('---'); + + try { + // Example 1: Simple chat completion + console.log('🤖 Example 1: Simple Chat Completion'); + const response = await client.createChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: + 'You are a helpful assistant that provides concise answers.', + }, + { + role: MessageRole.user, + content: 'Tell me a fun fact about TypeScript.', + }, + ], + max_tokens: 150, + }, + provider + ); + + console.log('Response:', response.choices[0].message.content); + console.log('Usage:', response.usage); + console.log('---\n'); + + // Example 2: Streaming chat completion + console.log('🌊 Example 2: Streaming Chat Completion'); + console.log('Assistant: '); + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: + 'You are a creative storyteller. Tell engaging short stories.', + }, + { + role: MessageRole.user, + content: 'Tell me a short story about a robot learning to paint.', + }, + ], + max_tokens: 200, + }, + { + onOpen: () => console.log('[Stream opened]'), + onContent: (content) => process.stdout.write(content), + onChunk: (chunk) => { + // Optional: log chunk metadata + if (chunk.id) { + // console.log(`\n[Chunk: ${chunk.id}]`); + } + }, + onUsageMetrics: (metrics) => { + console.log(`\n\n[Usage: ${metrics.total_tokens} tokens]`); + }, + onFinish: () => console.log('\n[Stream completed]'), + onError: (error) => console.error('\n[Stream error]:', error), + }, + provider + ); + + console.log('\n---\n'); + + // Example 3: Multi-turn conversation + console.log('💬 Example 3: Multi-turn Conversation'); + + const conversation = [ + { + role: MessageRole.system, + content: 'You are a helpful programming tutor.', + }, + { + role: MessageRole.user, + content: 'What is the difference between let and const in JavaScript?', + }, + ]; + + // First message + const firstResponse = await client.createChatCompletion( + { + model, + messages: conversation, + max_tokens: 200, + }, + provider + ); + + console.log('Tutor:', firstResponse.choices[0].message.content); + + // Add assistant response to conversation + conversation.push({ + role: MessageRole.assistant, + content: firstResponse.choices[0].message.content || '', + }); + + // Add follow-up question + conversation.push({ + role: MessageRole.user, + content: 'Can you give me a simple code example showing the difference?', + }); + + // Second message + const secondResponse = await client.createChatCompletion( + { + model, + messages: conversation, + max_tokens: 300, + }, + provider + ); + + console.log( + '\nTutor (follow-up):', + secondResponse.choices[0].message.content + ); + console.log('---\n'); + + // Example 4: Tool calls (function calling) + console.log('🔧 Example 4: Function Calling'); + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.user, + content: "What's the weather like in San Francisco and New York?", + }, + ], + tools: [ + { + type: ChatCompletionToolType.function, + function: { + name: 'get_weather', + description: 'Get the current weather in a given location', + parameters: { + type: 'object', + properties: { + location: { + type: 'string', + description: 'The city and state, e.g. San Francisco, CA', + }, + unit: { + type: 'string', + enum: ['celsius', 'fahrenheit'], + description: 'The unit of temperature', + }, + }, + required: ['location'], + }, + strict: true, + }, + }, + ], + }, + { + onTool: (toolCall) => { + console.log(`\n🔧 Tool called: ${toolCall.function.name}`); + console.log(`Arguments: ${toolCall.function.arguments}`); + + // In a real application, you would execute the function here + // and then continue the conversation with the result + try { + const args = JSON.parse(toolCall.function.arguments); + console.log(`[Simulated] Getting weather for: ${args.location}`); + } catch { + console.log('Could not parse tool arguments'); + } + }, + onReasoning: (reasoning) => { + if (reasoning.trim()) { + console.log(`\n🧠 Reasoning: ${reasoning}`); + } + }, + onContent: (content) => { + process.stdout.write(content); + }, + onFinish: () => console.log('\n[Function calling completed]'), + onError: (error) => console.error('\n[Error]:', error), + }, + provider + ); + } catch (error) { + console.error('Error in chat examples:', error); + process.exit(1); + } +}; + +// Run the main function +main().catch((error) => { + console.error('Unhandled error:', error); + process.exit(1); +}); diff --git a/examples/list/README.md b/examples/list/README.md index 4f91a66..206d540 100644 --- a/examples/list/README.md +++ b/examples/list/README.md @@ -1,10 +1,17 @@ # List Example -This example demonstrates how to use the Inference Gateway SDK for listing models and MCP tools. It includes making requests to the SDK and handling responses using the Typescript SDK. +This example demonstrates how to use the Inference Gateway SDK for listing models and MCP tools. It includes making requests to the SDK and handling responses using the TypeScript SDK. + +## Features Demonstrated + +1. **List All Models** - Retrieve all available models across providers +2. **List Provider-Specific Models** - Filter models by provider +3. **List MCP Tools** - Discover available Model Context Protocol tools +4. **Health Check** - Verify gateway connectivity ## Getting Started -1. Ensure you have the Inference Gateway running locally or have access to an instance, if not please read the [Quick Start](../README.md#quick-start) section in the main README. +1. Ensure you have the Inference Gateway running locally or have access to an instance. If not, please read the [Quick Start](../README.md#quick-start) section in the main README. 2. Install the SDK if you haven't already: @@ -12,8 +19,38 @@ This example demonstrates how to use the Inference Gateway SDK for listing model npm install ``` -3. Run the example: +3. (Optional) Set environment variables to see provider-specific examples: + + ```bash + export PROVIDER=groq + ``` + +4. Run the example: ```bash npm start ``` + +## Example Output + +The example will show: + +- Complete list of models grouped by provider +- Provider-specific model details (if PROVIDER is set) +- Available MCP tools (if EXPOSE_MCP is enabled) +- Gateway health status + +## MCP Tools + +To see MCP tools in action, ensure the gateway is started with: + +```bash +EXPOSE_MCP=true docker run --rm -p 8080:8080 --env-file .env ghcr.io/inference-gateway/inference-gateway:latest +``` + +## Supported Operations + +- **Model Discovery** - Find all available models +- **Provider Filtering** - Get models from specific providers +- **Tool Discovery** - List available MCP tools and their schemas +- **Health Monitoring** - Check gateway status diff --git a/examples/list/index.ts b/examples/list/index.ts index d758dbb..6c927ca 100644 --- a/examples/list/index.ts +++ b/examples/list/index.ts @@ -1,21 +1,128 @@ import { InferenceGatewayClient, Provider } from '@inference-gateway/sdk'; -(async () => { +const main = async () => { const client = new InferenceGatewayClient({ baseURL: 'http://localhost:8080/v1', }); const provider = process.env.PROVIDER as Provider; + console.log('🔍 Inference Gateway SDK - List Examples'); + console.log('=========================================\n'); + try { - // List all models - const models = await client.listModels(); - console.log('All models:', models); + // Example 1: List all models + console.log('📋 Example 1: List All Models'); + const allModels = await client.listModels(); + console.log( + `Found ${allModels.data.length} total models across all providers:` + ); + + // Group models by provider for better display + const modelsByProvider = allModels.data.reduce( + (acc, model) => { + const providerName = model.served_by; + if (!acc[providerName]) { + acc[providerName] = []; + } + acc[providerName].push(model); + return acc; + }, + {} as Record + ); + + Object.entries(modelsByProvider).forEach(([providerName, models]) => { + console.log(`\n ${providerName.toUpperCase()}: ${models.length} models`); + models.slice(0, 3).forEach((model) => { + console.log( + ` • ${model.id} (created: ${new Date(model.created * 1000).toLocaleDateString()})` + ); + }); + if (models.length > 3) { + console.log(` ... and ${models.length - 3} more`); + } + }); + + console.log('\n---\n'); + + // Example 2: List models from a specific provider (if provided) + if (provider) { + console.log(`📋 Example 2: List Models from ${provider.toUpperCase()}`); + const providerModels = await client.listModels(provider); + console.log( + `Found ${providerModels.data.length} models from ${provider}:` + ); + + providerModels.data.forEach((model) => { + console.log(` • ${model.id}`); + console.log(` Owner: ${model.owned_by}`); + console.log( + ` Created: ${new Date(model.created * 1000).toLocaleDateString()}` + ); + console.log(` Served by: ${model.served_by}`); + console.log(''); + }); + } else { + console.log('📋 Example 2: Skipped (no PROVIDER specified)'); + console.log( + 'Set PROVIDER environment variable to see provider-specific models' + ); + } + + console.log('---\n'); - // List models from a specific provider - const llms = await client.listModels(provider); - console.log(`Specific ${provider} models:`, llms); + // Example 3: List MCP tools (if available) + console.log('🛠️ Example 3: List MCP Tools'); + try { + const tools = await client.listTools(); + console.log(`Found ${tools.data.length} MCP tools:`); + + if (tools.data.length === 0) { + console.log( + ' No MCP tools available. Make sure EXPOSE_MCP=true on the gateway.' + ); + } else { + tools.data.forEach((tool) => { + console.log(`\n 🔧 ${tool.name}`); + console.log(` Description: ${tool.description}`); + console.log(` Server: ${tool.server}`); + + if (tool.input_schema) { + console.log( + ` Input schema: ${JSON.stringify(tool.input_schema, null, 2).substring(0, 100)}...` + ); + } + }); + } + } catch (error) { + console.log( + ' ❌ MCP tools not available (EXPOSE_MCP might be disabled)' + ); + console.log( + ` Error: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } + + console.log('\n---\n'); + + // Example 4: Health check + console.log('❤️ Example 4: Health Check'); + const isHealthy = await client.healthCheck(); + console.log( + `Gateway health status: ${isHealthy ? '✅ Healthy' : '❌ Unhealthy'}` + ); + + console.log('\n---\n'); } catch (error) { - console.error('Error:', error); + console.error('❌ Error in list examples:', error); + process.exit(1); } -})(); + + console.log('\n✅ All examples completed successfully!'); +}; + +// Run the main function +main().catch((error) => { + console.error('Unhandled error:', error); + process.exit(1); +}); diff --git a/examples/mcp/README.md b/examples/mcp/README.md index 5949837..5e9c87d 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -1,7 +1,287 @@ # MCP Example -This example demonstrates how to use the Inference Gateway SDK for listing models and MCP tools. It includes making requests to the SDK and handling responses using the Typescript SDK. +This example demonstrates how to use the Inference Gateway SDK with Model Context Protocol (MCP) tools in a multi-provider architecture. It showcases how to connect to MCP servers, discover available tools, and use them in AI conversations. + +## Features Demonstrated + +1. **MCP Tool Discovery** - List and explore available MCP tools +2. **File Operations** - Use filesystem MCP server for file operations +3. **Web Scraping** - Fetch content from URLs using MCP tools +4. **Multi-Tool Conversations** - Combine multiple MCP tools in single conversations +5. **Tool Function Calling** - Stream responses with real-time tool execution + +## Architecture + +This example uses Docker Compose to orchestrate: + +- **Inference Gateway** - Main API gateway with MCP support enabled +- **MCP Filesystem Server** - Provides file system operations +- **MCP Web Search Server** - Simulated web search and URL fetching +- **Optional Ollama** - Local model inference (when using `--profile with-ollama`) ## Getting Started -1. For this example you should have docker compose installed, if you're using the dev container in VS Code you should be all set. +### Prerequisites + +- Docker and Docker Compose installed +- API key for at least one provider (OpenAI, Groq, Anthropic, etc.) + +### 1. Environment Setup + +Copy the parent `.env.example` to `.env` and configure your API keys: + +```bash +cp ../.env.example ../.env +``` + +Edit `../.env` and add your API keys: + +```bash +GROQ_API_KEY=your_groq_api_key_here +OPENAI_API_KEY=your_openai_api_key_here +ANTHROPIC_API_KEY=your_anthropic_api_key_here +``` + +### 2. Start the MCP Infrastructure + +Start all services using Docker Compose: + +```bash +npm run compose:up +``` + +This will start: + +- Inference Gateway on port 8080 +- MCP Filesystem server on port 3000 +- MCP Web Search server on port 3001 + +### 3. Install Dependencies + +```bash +npm install +``` + +### 4. Configure Provider and Model + +Set your preferred provider and model: + +```bash +export PROVIDER=groq +export LLM=groq/meta-llama/llama-3.3-70b-versatile +``` + +Or for OpenAI: + +```bash +export PROVIDER=openai +export LLM=gpt-4o +``` + +### 5. Run the Example + +```bash +npm start +``` + +## Available Commands + +- `npm start` - Run the MCP example +- `npm run compose:up` - Start all services in background +- `npm run compose:down` - Stop all services +- `npm run compose:logs` - View logs from all services + +## Example Output + +The example will demonstrate: + +``` +=== MCP Tools Example === + +🔍 Checking gateway health... +Gateway health: ✅ Healthy + +📋 Listing available MCP tools... +Found 4 MCP tools: + +1. read_file + Description: Read content from a file + Server: http://mcp-filesystem:3000/mcp + Schema: { + "type": "object", + "properties": { + "file_path": { + "type": "string", + "description": "Path to the file to read" + } + }, + "required": ["file_path"] + } + +2. write_file + Description: Write content to a file + Server: http://mcp-filesystem:3000/mcp + ... + +=== Example 1: File Operations with MCP === + +🚀 Starting file reading example... +I'll help you read the contents of /tmp/example.txt file. + +🔧 Tool called: read_file +📝 Arguments: {"file_path": "/tmp/example.txt"} + +The file contains: +Hello from MCP filesystem server! +This is a sample file for testing MCP file operations. +Created at: Mon May 27 10:30:00 UTC 2025 + +✅ File reading example completed +``` + +## MCP Servers Included + +### Filesystem Server + +- **Purpose**: File system operations (read, write, list directories) +- **Port**: 3000 +- **Tools**: `read_file`, `write_file`, `list_directory` +- **Allowed Directories**: `/shared`, `/tmp` + +### Web Search Server + +- **Purpose**: Web content fetching and search simulation +- **Port**: 3001 +- **Tools**: `fetch_url`, `search_web` +- **Features**: HTTP requests, basic content extraction + +## Supported Providers + +All Inference Gateway providers work with MCP tools: + +- `openai` - GPT models with excellent tool calling +- `groq` - Fast inference with Llama and Mixtral models +- `anthropic` - Claude models with strong reasoning +- `ollama` - Local models (use `--profile with-ollama`) +- `cohere` - Command models +- `deepseek` - DeepSeek models +- `cloudflare` - Workers AI models + +## Using with Local Models (Ollama) + +To include Ollama for local model inference: + +```bash +docker-compose --profile with-ollama up -d +``` + +Then pull a model: + +```bash +docker exec -it mcp_ollama_1 ollama pull llama3.2 +``` + +Set environment variables: + +```bash +export PROVIDER=ollama +export LLM=llama3.2 +``` + +## Troubleshooting + +### MCP Tools Not Available + +If you see "No MCP tools available": + +1. Check if MCP servers are running: + + ```bash + docker-compose ps + ``` + +2. Verify MCP server logs: + + ```bash + npm run compose:logs + ``` + +3. Ensure the Inference Gateway can reach MCP servers: + ```bash + docker exec inference-gateway-mcp curl -f http://mcp-filesystem:3000/mcp + ``` + +### Gateway Health Check Fails + +If the gateway appears unhealthy: + +1. Check gateway logs: + + ```bash + docker-compose logs inference-gateway + ``` + +2. Verify API keys are set in `.env` file +3. Test direct connection: + ```bash + curl http://localhost:8080/health + ``` + +### Tool Calls Not Working + +If tool calls fail during conversations: + +1. Verify the model supports tool calling (GPT-4, Claude, etc.) +2. Check MCP server responses: + + ```bash + curl -X POST http://localhost:3000/mcp/tools/list + ``` + +3. Enable debug logging by adding to docker-compose.yml: + ```yaml + environment: + - LOG_LEVEL=debug + ``` + +## Custom MCP Servers + +To add your own MCP server: + +1. Add service to `docker-compose.yml`: + + ```yaml + my-custom-mcp: + image: my-mcp-server:latest + ports: + - '3002:3002' + networks: + - inference-network + ``` + +2. Update Inference Gateway environment: + + ```yaml + MCP_SERVERS: 'filesystem=http://mcp-filesystem:3000/mcp,web-search=http://mcp-web-search:3001/mcp,custom=http://my-custom-mcp:3002/mcp' + ``` + +3. Restart services: + ```bash + npm run compose:down + npm run compose:up + ``` + +## Notes + +- MCP tools are called automatically by AI models when relevant to the conversation +- Tool schemas are defined by the MCP servers and exposed through the `/mcp/tools` endpoint +- Each MCP server can provide multiple tools with different capabilities +- The Inference Gateway acts as a bridge between AI models and MCP tools +- Tool execution is streamed in real-time during conversations +- File operations are sandboxed to allowed directories for security + +## Resources + +- [Model Context Protocol Specification](https://modelcontextprotocol.io/) +- [Inference Gateway Documentation](https://github.com/inference-gateway/inference-gateway) +- [Official MCP Servers](https://github.com/modelcontextprotocol/servers) diff --git a/examples/mcp/docker-compose.yml b/examples/mcp/docker-compose.yml new file mode 100644 index 0000000..fd9f3e6 --- /dev/null +++ b/examples/mcp/docker-compose.yml @@ -0,0 +1,213 @@ +version: '3.8' + +services: + # Inference Gateway + inference-gateway: + image: ghcr.io/inference-gateway/inference-gateway:latest + ports: + - '8080:8080' + environment: + # Enable MCP support + MCP_ENABLE: 'true' + MCP_EXPOSE: 'true' + MCP_SERVERS: 'filesystem=http://mcp-filesystem:3000/mcp,web-search=http://mcp-web-search:3001/mcp' + + # Server settings + SERVER_HOST: '0.0.0.0' + SERVER_PORT: '8080' + + # Provider settings - Add your API keys here + GROQ_API_URL: 'https://api.groq.com/openai/v1' + GROQ_API_KEY: '${GROQ_API_KEY:-}' + + OPENAI_API_URL: 'https://api.openai.com/v1' + OPENAI_API_KEY: '${OPENAI_API_KEY:-}' + + ANTHROPIC_API_URL: 'https://api.anthropic.com/v1' + ANTHROPIC_API_KEY: '${ANTHROPIC_API_KEY:-}' + + # Optional: Ollama for local models + OLLAMA_API_URL: 'http://ollama:11434/v1' + OLLAMA_API_KEY: '' + depends_on: + - mcp-filesystem + - mcp-web-search + networks: + - inference-network + volumes: + - shared-data:/shared + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:8080/health'] + interval: 30s + timeout: 10s + retries: 3 + + # MCP Filesystem Server + mcp-filesystem: + image: ghcr.io/modelcontextprotocol/servers/filesystem:latest + ports: + - '3000:3000' + environment: + MCP_SERVER_NAME: 'filesystem' + MCP_SERVER_VERSION: '1.0.0' + ALLOWED_DIRECTORIES: '/shared,/tmp' + networks: + - inference-network + volumes: + - shared-data:/shared + - ./mcp-data:/tmp + command: > + sh -c " + mkdir -p /tmp && + echo 'Hello from MCP filesystem server!' > /tmp/example.txt && + echo 'This is a sample file for testing MCP file operations.' >> /tmp/example.txt && + echo 'Created at: $(date)' >> /tmp/example.txt && + mcp-server-filesystem --allowed-directories /shared,/tmp + " + + # MCP Web Search Server (example using a hypothetical web search MCP server) + mcp-web-search: + image: node:18-alpine + ports: + - '3001:3001' + working_dir: /app + environment: + NODE_ENV: 'production' + MCP_SERVER_NAME: 'web-search' + MCP_SERVER_VERSION: '1.0.0' + networks: + - inference-network + volumes: + - ./mcp-servers/web-search:/app + command: > + sh -c " + if [ ! -f package.json ]; then + npm init -y && + npm install express cors axios cheerio && + cat > index.js << 'EOF' + const express = require('express'); + const cors = require('cors'); + const axios = require('axios'); + + const app = express(); + app.use(cors()); + app.use(express.json()); + + // MCP server info + app.get('/mcp', (req, res) => { + res.json({ + capabilities: { + tools: { + listChanged: false + } + }, + serverInfo: { + name: 'web-search', + version: '1.0.0' + } + }); + }); + + // List available tools + app.post('/mcp/tools/list', (req, res) => { + res.json({ + tools: [ + { + name: 'fetch_url', + description: 'Fetch content from a URL', + inputSchema: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'The URL to fetch' + } + }, + required: ['url'] + } + }, + { + name: 'search_web', + description: 'Search the web for information', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The search query' + } + }, + required: ['query'] + } + } + ] + }); + }); + + // Execute tools + app.post('/mcp/tools/call', async (req, res) => { + try { + const { name, arguments: args } = req.body; + + if (name === 'fetch_url') { + const response = await axios.get(args.url, { timeout: 10000 }); + res.json({ + content: [ + { + type: 'text', + text: typeof response.data === 'object' + ? JSON.stringify(response.data, null, 2) + : response.data.toString().substring(0, 5000) + } + ] + }); + } else if (name === 'search_web') { + // Simulate web search (in real implementation, you'd use a real search API) + res.json({ + content: [ + { + type: 'text', + text: \`Search results for \"\${args.query}\":\n\n1. Example result for \${args.query}\n2. Another relevant result\n3. More information about \${args.query}\` + } + ] + }); + } else { + res.status(400).json({ error: 'Unknown tool: ' + name }); + } + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + const port = process.env.PORT || 3001; + app.listen(port, '0.0.0.0', () => { + console.log(\`MCP Web Search server running on port \${port}\`); + }); + EOF + fi && + node index.js + " + + # Optional: Ollama for local models + ollama: + image: ollama/ollama:latest + ports: + - '11434:11434' + networks: + - inference-network + volumes: + - ollama-data:/root/.ollama + environment: + OLLAMA_HOST: '0.0.0.0' + profiles: + - 'with-ollama' + +volumes: + shared-data: + driver: local + ollama-data: + driver: local + +networks: + inference-network: + driver: bridge diff --git a/examples/mcp/index.ts b/examples/mcp/index.ts new file mode 100644 index 0000000..3c6c37c --- /dev/null +++ b/examples/mcp/index.ts @@ -0,0 +1,255 @@ +import { + ChatCompletionToolType, + InferenceGatewayClient, + MessageRole, + Provider, +} from '@inference-gateway/sdk'; + +(async () => { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + const provider = (process.env.PROVIDER as Provider) || Provider.groq; + const model = process.env.LLM || 'groq/meta-llama/llama-3.3-70b-versatile'; + + console.log('=== MCP Tools Example ===\n'); + + try { + // First, let's check if the gateway is healthy + console.log('🔍 Checking gateway health...'); + const isHealthy = await client.healthCheck(); + console.log( + `Gateway health: ${isHealthy ? '✅ Healthy' : '❌ Unhealthy'}\n` + ); + + if (!isHealthy) { + console.log( + 'Please ensure the Inference Gateway is running with Docker Compose.' + ); + process.exit(1); + } + + // List available MCP tools + console.log('📋 Listing available MCP tools...'); + const tools = await client.listTools(); + console.log(`Found ${tools.data.length} MCP tools:\n`); + + tools.data.forEach((tool, index) => { + console.log(`${index + 1}. ${tool.name}`); + console.log(` Description: ${tool.description}`); + console.log(` Server: ${tool.server}`); + console.log(` Schema: ${JSON.stringify(tool.input_schema, null, 2)}\n`); + }); + + if (tools.data.length === 0) { + console.log( + '⚠️ No MCP tools available. Ensure MCP servers are configured and running.' + ); + return; + } + + // Example 1: Use MCP tools for file operations (if filesystem MCP server is available) + const fileReadTool = tools.data.find((tool) => tool.name === 'read_file'); + if (fileReadTool) { + console.log('=== Example 1: File Operations with MCP ===\n'); + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: + 'You are a helpful assistant that can read files using MCP tools. When asked to read a file, use the read_file tool.', + }, + { + role: MessageRole.user, + content: + 'Can you read the contents of /tmp/example.txt if it exists?', + }, + ], + tools: [ + { + type: ChatCompletionToolType.function, + function: { + name: fileReadTool.name, + description: fileReadTool.description, + parameters: fileReadTool.input_schema, + strict: true, + }, + }, + ], + }, + { + onOpen: () => console.log('🚀 Starting file reading example...'), + onContent: (content) => process.stdout.write(content), + onTool: (toolCall) => { + console.log(`\n🔧 Tool called: ${toolCall.function.name}`); + console.log(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onFinish: () => console.log('\n✅ File reading example completed\n'), + onError: (error) => console.error('❌ Error:', error), + }, + provider + ); + } + + // Example 2: Use MCP tools for web scraping (if web scraper MCP server is available) + const webScrapeTool = tools.data.find( + (tool) => tool.name.includes('fetch') || tool.name.includes('scrape') + ); + if (webScrapeTool) { + console.log('=== Example 2: Web Scraping with MCP ===\n'); + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: + 'You are a helpful assistant that can fetch web content using MCP tools. Use the available tools to gather information from websites.', + }, + { + role: MessageRole.user, + content: + 'Can you fetch information from https://httpbin.org/json and tell me what you find?', + }, + ], + tools: [ + { + type: ChatCompletionToolType.function, + function: { + name: webScrapeTool.name, + description: webScrapeTool.description, + parameters: webScrapeTool.input_schema, + strict: true, + }, + }, + ], + }, + { + onOpen: () => console.log('🚀 Starting web scraping example...'), + onContent: (content) => process.stdout.write(content), + onTool: (toolCall) => { + console.log(`\n🔧 Tool called: ${toolCall.function.name}`); + console.log(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onFinish: () => console.log('\n✅ Web scraping example completed\n'), + onError: (error) => console.error('❌ Error:', error), + }, + provider + ); + } + + // Example 3: Generic MCP tool usage - use the first available tool + if (tools.data.length > 0 && !fileReadTool && !webScrapeTool) { + console.log('=== Example 3: Generic MCP Tool Usage ===\n'); + + const firstTool = tools.data[0]; + console.log(`Using tool: ${firstTool.name}\n`); + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: `You are a helpful assistant that has access to the ${firstTool.name} tool. Use it when appropriate to help the user.`, + }, + { + role: MessageRole.user, + content: `Can you help me use the ${firstTool.name} tool? What can it do?`, + }, + ], + tools: [ + { + type: ChatCompletionToolType.function, + function: { + name: firstTool.name, + description: firstTool.description, + parameters: firstTool.input_schema, + strict: true, + }, + }, + ], + }, + { + onOpen: () => console.log('🚀 Starting generic tool example...'), + onContent: (content) => process.stdout.write(content), + onTool: (toolCall) => { + console.log(`\n🔧 Tool called: ${toolCall.function.name}`); + console.log(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onFinish: () => console.log('\n✅ Generic tool example completed\n'), + onError: (error) => console.error('❌ Error:', error), + }, + provider + ); + } + + // Example 4: Multi-tool conversation + if (tools.data.length > 1) { + console.log('=== Example 4: Multi-Tool Conversation ===\n'); + + const availableTools = tools.data.slice(0, 3).map((tool) => ({ + type: ChatCompletionToolType.function, + function: { + name: tool.name, + description: tool.description, + parameters: tool.input_schema, + strict: true, + }, + })); + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: `You are a helpful assistant with access to multiple MCP tools: ${tools.data + .slice(0, 3) + .map((t) => t.name) + .join( + ', ' + )}. Use these tools to help the user accomplish their tasks.`, + }, + { + role: MessageRole.user, + content: + 'I need help with data analysis. Can you show me what tools are available and suggest how to use them?', + }, + ], + tools: availableTools, + }, + { + onOpen: () => console.log('🚀 Starting multi-tool conversation...'), + onContent: (content) => process.stdout.write(content), + onTool: (toolCall) => { + console.log(`\n🔧 Tool called: ${toolCall.function.name}`); + console.log(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onFinish: () => + console.log('\n✅ Multi-tool conversation completed\n'), + onError: (error) => console.error('❌ Error:', error), + }, + provider + ); + } + } catch (error) { + if ( + error instanceof Error && + error.message.includes('MCP tools endpoint is not exposed') + ) { + console.error( + '❌ MCP tools are not exposed. Please ensure the Inference Gateway is started with EXPOSE_MCP=true' + ); + console.log('\n💡 To fix this, restart the gateway with:'); + console.log(' docker-compose up --build'); + } else { + console.error('❌ Error:', error); + } + } +})(); diff --git a/examples/mcp/package-lock.json b/examples/mcp/package-lock.json new file mode 100644 index 0000000..436dc9e --- /dev/null +++ b/examples/mcp/package-lock.json @@ -0,0 +1,561 @@ +{ + "name": "mcp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@inference-gateway/sdk": "^0.7.1" + }, + "devDependencies": { + "tsx": "^4.19.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inference-gateway/sdk": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.1.tgz", + "integrity": "sha512-O6wHlmB5XmQApASaw6yhTaRHMFkSzLUl9DNGb2RYN3/0wK5Bdlymed8HCl69dbATfkEh3eXU9SiZ8FG/pww7Lg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0", + "npm": ">=10.9.0" + }, + "peerDependencies": { + "node-fetch": "^2.7.0" + }, + "peerDependenciesMeta": { + "node-fetch": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + } + } +} diff --git a/examples/mcp/package.json b/examples/mcp/package.json new file mode 100644 index 0000000..227308b --- /dev/null +++ b/examples/mcp/package.json @@ -0,0 +1,22 @@ +{ + "name": "mcp", + "version": "1.0.0", + "description": "This example demonstrates how to use the Inference Gateway SDK with Model Context Protocol (MCP) tools in a multi-provider architecture.", + "main": "index.js", + "private": true, + "scripts": { + "start": "tsx index.ts", + "compose:up": "docker-compose up -d", + "compose:down": "docker-compose down", + "compose:logs": "docker-compose logs -f" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@inference-gateway/sdk": "^0.7.1" + }, + "devDependencies": { + "tsx": "^4.19.4" + } +} diff --git a/examples/mcp/shared/README.md b/examples/mcp/shared/README.md new file mode 100644 index 0000000..a42e083 --- /dev/null +++ b/examples/mcp/shared/README.md @@ -0,0 +1,13 @@ +# Sample Data for MCP Example + +This directory contains sample files that can be accessed by the MCP filesystem server. + +## Files + +- `sample.txt` - A simple text file for testing file reading +- `data.json` - JSON data for testing structured file operations +- `config.yaml` - YAML configuration example + +## Usage + +These files can be accessed through MCP tools when the filesystem server is running. The AI models can read, analyze, and work with this data using the MCP file operations. diff --git a/examples/mcp/shared/config.yaml b/examples/mcp/shared/config.yaml new file mode 100644 index 0000000..d41e170 --- /dev/null +++ b/examples/mcp/shared/config.yaml @@ -0,0 +1,49 @@ +# MCP Configuration Example +# This YAML file demonstrates configuration data that can be read by MCP tools + +api: + version: 'v1' + base_url: 'http://localhost:8080' + timeout: 30s + +mcp: + enabled: true + expose: true + servers: + - name: filesystem + url: 'http://mcp-filesystem:3000/mcp' + timeout: 10s + - name: web-search + url: 'http://mcp-web-search:3001/mcp' + timeout: 15s + +providers: + groq: + api_url: 'https://api.groq.com/openai/v1' + models: + - 'meta-llama/llama-3.3-70b-versatile' + - 'meta-llama/llama-3.2-90b-vision-preview' + + openai: + api_url: 'https://api.openai.com/v1' + models: + - 'gpt-4o' + - 'gpt-4o-mini' + - 'gpt-3.5-turbo' + + anthropic: + api_url: 'https://api.anthropic.com/v1' + models: + - 'claude-3-5-sonnet-20241022' + - 'claude-3-5-haiku-20241022' + +logging: + level: info + format: json + +security: + cors_enabled: true + auth_required: false + allowed_origins: + - 'http://localhost:3000' + - 'http://localhost:8080' diff --git a/examples/mcp/shared/data.json b/examples/mcp/shared/data.json new file mode 100644 index 0000000..dee631c --- /dev/null +++ b/examples/mcp/shared/data.json @@ -0,0 +1,42 @@ +{ + "project": { + "name": "Inference Gateway TypeScript SDK", + "version": "0.7.1", + "description": "TypeScript SDK for the Inference Gateway", + "repository": "https://github.com/inference-gateway/typescript-sdk" + }, + "examples": { + "mcp": { + "description": "Model Context Protocol integration example", + "features": [ + "Tool discovery", + "File operations", + "Web scraping", + "Multi-tool conversations" + ], + "servers": [ + { + "name": "filesystem", + "port": 3000, + "tools": ["read_file", "write_file", "list_directory"] + }, + { + "name": "web-search", + "port": 3001, + "tools": ["fetch_url", "search_web"] + } + ] + } + }, + "providers": [ + "openai", + "groq", + "anthropic", + "ollama", + "cohere", + "deepseek", + "cloudflare" + ], + "created": "2025-05-27", + "purpose": "Demonstrate MCP capabilities with Inference Gateway" +} diff --git a/examples/mcp/shared/sample.txt b/examples/mcp/shared/sample.txt new file mode 100644 index 0000000..1b0bc79 --- /dev/null +++ b/examples/mcp/shared/sample.txt @@ -0,0 +1,18 @@ +Hello from the MCP filesystem server! + +This is a sample text file that demonstrates how MCP tools can interact with the file system. + +The file was created as part of the Inference Gateway TypeScript SDK examples. + +You can ask the AI assistant to: +- Read this file's contents +- Analyze the text +- Summarize the information +- Count words or lines +- Extract specific information + +The MCP filesystem server allows secure access to this shared directory, +enabling AI models to work with real file data through the Model Context Protocol. + +Created: May 27, 2025 +Purpose: Demonstration and testing of MCP file operations diff --git a/examples/mcp/tsconfig.json b/examples/mcp/tsconfig.json new file mode 100644 index 0000000..3a2170b --- /dev/null +++ b/examples/mcp/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["index.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/package-lock.json b/examples/package-lock.json new file mode 100644 index 0000000..dcff41e --- /dev/null +++ b/examples/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "inference-gateway-examples", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "inference-gateway-examples", + "version": "1.0.0", + "license": "MIT" + } + } +} diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 0000000..89e2474 --- /dev/null +++ b/examples/package.json @@ -0,0 +1,25 @@ +{ + "name": "inference-gateway-examples", + "version": "1.0.0", + "description": "Examples for the Inference Gateway TypeScript SDK", + "private": true, + "scripts": { + "install-all": "npm install && cd list && npm install && cd ../chat && npm install && cd ../mcp && npm install", + "start:list": "cd list && npm start", + "start:chat": "cd chat && npm start", + "start:mcp": "cd mcp && npm start", + "mcp:up": "cd mcp && npm run compose:up", + "mcp:down": "cd mcp && npm run compose:down", + "mcp:logs": "cd mcp && npm run compose:logs", + "check-gateway": "curl -f http://localhost:8080/health || echo 'Gateway not running'" + }, + "keywords": [ + "inference-gateway", + "ai", + "llm", + "typescript", + "examples" + ], + "author": "Inference Gateway Team", + "license": "MIT" +} diff --git a/openapi.yaml b/openapi.yaml index 33537d5..6bcde18 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -17,13 +17,13 @@ info: servers: - url: http://localhost:8080 description: Default server without version prefix for healthcheck and proxy and points - x-server-tags: ["Health", "Proxy"] + x-server-tags: ['Health', 'Proxy'] - url: http://localhost:8080/v1 description: Default server with version prefix for listing models and chat completions - x-server-tags: ["Models", "Completions"] + x-server-tags: ['Models', 'Completions'] - url: https://api.inference-gateway.local/v1 description: Local server with version prefix for listing models and chat completions - x-server-tags: ["Models", "Completions"] + x-server-tags: ['Models', 'Completions'] tags: - name: Models description: List and describe the various models available in the API. @@ -54,70 +54,70 @@ paths: in: query required: false schema: - $ref: "#/components/schemas/Provider" + $ref: '#/components/schemas/Provider' description: Specific provider to query (optional) responses: - "200": + '200': description: List of available models content: application/json: schema: - $ref: "#/components/schemas/ListModelsResponse" + $ref: '#/components/schemas/ListModelsResponse' examples: allProviders: summary: Models from all providers value: - object: "list" + object: 'list' data: - - id: "openai/gpt-4o" - object: "model" + - id: 'openai/gpt-4o' + object: 'model' created: 1686935002 - owned_by: "openai" - served_by: "openai" - - id: "openai/llama-3.3-70b-versatile" - object: "model" + owned_by: 'openai' + served_by: 'openai' + - id: 'openai/llama-3.3-70b-versatile' + object: 'model' created: 1723651281 - owned_by: "groq" - served_by: "groq" - - id: "cohere/claude-3-opus-20240229" - object: "model" + owned_by: 'groq' + served_by: 'groq' + - id: 'cohere/claude-3-opus-20240229' + object: 'model' created: 1708905600 - owned_by: "anthropic" - served_by: "anthropic" - - id: "cohere/command-r" - object: "model" + owned_by: 'anthropic' + served_by: 'anthropic' + - id: 'cohere/command-r' + object: 'model' created: 1707868800 - owned_by: "cohere" - served_by: "cohere" - - id: "ollama/phi3:3.8b" - object: "model" + owned_by: 'cohere' + served_by: 'cohere' + - id: 'ollama/phi3:3.8b' + object: 'model' created: 1718441600 - owned_by: "ollama" - served_by: "ollama" + owned_by: 'ollama' + served_by: 'ollama' singleProvider: summary: Models from a specific provider value: - object: "list" + object: 'list' data: - - id: "openai/gpt-4o" - object: "model" + - id: 'openai/gpt-4o' + object: 'model' created: 1686935002 - owned_by: "openai" - served_by: "openai" - - id: "openai/gpt-4-turbo" - object: "model" + owned_by: 'openai' + served_by: 'openai' + - id: 'openai/gpt-4-turbo' + object: 'model' created: 1687882410 - owned_by: "openai" - served_by: "openai" - - id: "openai/gpt-3.5-turbo" - object: "model" + owned_by: 'openai' + served_by: 'openai' + - id: 'openai/gpt-3.5-turbo' + object: 'model' created: 1677649963 - owned_by: "openai" - served_by: "openai" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalError" + owned_by: 'openai' + served_by: 'openai' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' /chat/completions: post: operationId: createChatCompletion @@ -134,26 +134,26 @@ paths: in: query required: false schema: - $ref: "#/components/schemas/Provider" + $ref: '#/components/schemas/Provider' description: Specific provider to use (default determined by model) requestBody: - $ref: "#/components/requestBodies/CreateChatCompletionRequest" + $ref: '#/components/requestBodies/CreateChatCompletionRequest' responses: - "200": + '200': description: Successful response content: application/json: schema: - $ref: "#/components/schemas/CreateChatCompletionResponse" + $ref: '#/components/schemas/CreateChatCompletionResponse' text/event-stream: schema: - $ref: "#/components/schemas/SSEvent" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalError" + $ref: '#/components/schemas/SSEvent' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' /mcp/tools: get: operationId: listTools @@ -165,25 +165,25 @@ paths: security: - bearerAuth: [] responses: - "200": + '200': description: Successful response content: application/json: schema: - $ref: "#/components/schemas/ListToolsResponse" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/MCPNotExposed" - "500": - $ref: "#/components/responses/InternalError" + $ref: '#/components/schemas/ListToolsResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/MCPNotExposed' + '500': + $ref: '#/components/responses/InternalError' /proxy/{provider}/{path}: parameters: - name: provider in: path required: true schema: - $ref: "#/components/schemas/Provider" + $ref: '#/components/schemas/Provider' - name: path in: path required: true @@ -202,14 +202,14 @@ paths: If you decide to use this approach, please follow the provider-specific documentations. summary: Proxy GET request to provider responses: - "200": - $ref: "#/components/responses/ProviderResponse" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalError" + '200': + $ref: '#/components/responses/ProviderResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' security: - bearerAuth: [] post: @@ -222,16 +222,16 @@ paths: If you decide to use this approach, please follow the provider-specific documentations. summary: Proxy POST request to provider requestBody: - $ref: "#/components/requestBodies/ProviderRequest" + $ref: '#/components/requestBodies/ProviderRequest' responses: - "200": - $ref: "#/components/responses/ProviderResponse" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalError" + '200': + $ref: '#/components/responses/ProviderResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' security: - bearerAuth: [] put: @@ -244,16 +244,16 @@ paths: If you decide to use this approach, please follow the provider-specific documentations. summary: Proxy PUT request to provider requestBody: - $ref: "#/components/requestBodies/ProviderRequest" + $ref: '#/components/requestBodies/ProviderRequest' responses: - "200": - $ref: "#/components/responses/ProviderResponse" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalError" + '200': + $ref: '#/components/responses/ProviderResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' security: - bearerAuth: [] delete: @@ -266,14 +266,14 @@ paths: If you decide to use this approach, please follow the provider-specific documentations. summary: Proxy DELETE request to provider responses: - "200": - $ref: "#/components/responses/ProviderResponse" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalError" + '200': + $ref: '#/components/responses/ProviderResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' security: - bearerAuth: [] patch: @@ -286,16 +286,16 @@ paths: If you decide to use this approach, please follow the provider-specific documentations. summary: Proxy PATCH request to provider requestBody: - $ref: "#/components/requestBodies/ProviderRequest" + $ref: '#/components/requestBodies/ProviderRequest' responses: - "200": - $ref: "#/components/responses/ProviderResponse" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalError" + '200': + $ref: '#/components/responses/ProviderResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' security: - bearerAuth: [] /health: @@ -308,7 +308,7 @@ paths: Returns a 200 status code if the service is healthy summary: Health check responses: - "200": + '200': description: Health check successful components: requestBodies: @@ -341,18 +341,18 @@ components: openai: summary: OpenAI chat completion request value: - model: "gpt-3.5-turbo" + model: 'gpt-3.5-turbo' messages: - - role: "user" - content: "Hello! How can I assist you today?" + - role: 'user' + content: 'Hello! How can I assist you today?' temperature: 0.7 anthropic: summary: Anthropic Claude request value: - model: "claude-3-opus-20240229" + model: 'claude-3-opus-20240229' messages: - - role: "user" - content: "Explain quantum computing" + - role: 'user' + content: 'Explain quantum computing' temperature: 0.5 CreateChatCompletionRequest: required: true @@ -362,34 +362,34 @@ components: content: application/json: schema: - $ref: "#/components/schemas/CreateChatCompletionRequest" + $ref: '#/components/schemas/CreateChatCompletionRequest' responses: BadRequest: description: Bad request content: application/json: schema: - $ref: "#/components/schemas/Error" + $ref: '#/components/schemas/Error' Unauthorized: description: Unauthorized content: application/json: schema: - $ref: "#/components/schemas/Error" + $ref: '#/components/schemas/Error' InternalError: description: Internal server error content: application/json: schema: - $ref: "#/components/schemas/Error" + $ref: '#/components/schemas/Error' MCPNotExposed: description: MCP tools endpoint is not exposed content: application/json: schema: - $ref: "#/components/schemas/Error" + $ref: '#/components/schemas/Error' example: - error: "MCP tools endpoint is not exposed. Set EXPOSE_MCP=true to enable." + error: 'MCP tools endpoint is not exposed. Set EXPOSE_MCP=true to enable.' ProviderResponse: description: | ProviderResponse depends on the specific provider and endpoint being called @@ -397,26 +397,26 @@ components: content: application/json: schema: - $ref: "#/components/schemas/ProviderSpecificResponse" + $ref: '#/components/schemas/ProviderSpecificResponse' examples: openai: summary: OpenAI API response value: { - "id": "chatcmpl-123", - "object": "chat.completion", - "created": 1677652288, - "model": "gpt-3.5-turbo", - "choices": + 'id': 'chatcmpl-123', + 'object': 'chat.completion', + 'created': 1677652288, + 'model': 'gpt-3.5-turbo', + 'choices': [ { - "index": 0, - "message": + 'index': 0, + 'message': { - "role": "assistant", - "content": "Hello! How can I help you today?", + 'role': 'assistant', + 'content': 'Hello! How can I help you today?', }, - "finish_reason": "stop", + 'finish_reason': 'stop', }, ], } @@ -442,96 +442,96 @@ components: - deepseek x-provider-configs: ollama: - id: "ollama" - url: "http://ollama:8080/v1" - auth_type: "none" + id: 'ollama' + url: 'http://ollama:8080/v1' + auth_type: 'none' endpoints: models: - name: "list_models" - method: "GET" - endpoint: "/models" + name: 'list_models' + method: 'GET' + endpoint: '/models' chat: - name: "chat_completions" - method: "POST" - endpoint: "/chat/completions" + name: 'chat_completions' + method: 'POST' + endpoint: '/chat/completions' anthropic: - id: "anthropic" - url: "https://api.anthropic.com/v1" - auth_type: "bearer" + id: 'anthropic' + url: 'https://api.anthropic.com/v1' + auth_type: 'bearer' endpoints: models: - name: "list_models" - method: "GET" - endpoint: "/models" + name: 'list_models' + method: 'GET' + endpoint: '/models' chat: - name: "chat_completions" - method: "POST" - endpoint: "/chat/completions" + name: 'chat_completions' + method: 'POST' + endpoint: '/chat/completions' cohere: - id: "cohere" - url: "https://api.cohere.ai" - auth_type: "bearer" + id: 'cohere' + url: 'https://api.cohere.ai' + auth_type: 'bearer' endpoints: models: - name: "list_models" - method: "GET" - endpoint: "/v1/models" + name: 'list_models' + method: 'GET' + endpoint: '/v1/models' chat: - name: "chat_completions" - method: "POST" - endpoint: "/compatibility/v1/chat/completions" + name: 'chat_completions' + method: 'POST' + endpoint: '/compatibility/v1/chat/completions' groq: - id: "groq" - url: "https://api.groq.com/openai/v1" - auth_type: "bearer" + id: 'groq' + url: 'https://api.groq.com/openai/v1' + auth_type: 'bearer' endpoints: models: - name: "list_models" - method: "GET" - endpoint: "/models" + name: 'list_models' + method: 'GET' + endpoint: '/models' chat: - name: "chat_completions" - method: "POST" - endpoint: "/chat/completions" + name: 'chat_completions' + method: 'POST' + endpoint: '/chat/completions' openai: - id: "openai" - url: "https://api.openai.com/v1" - auth_type: "bearer" + id: 'openai' + url: 'https://api.openai.com/v1' + auth_type: 'bearer' endpoints: models: - name: "list_models" - method: "GET" - endpoint: "/models" + name: 'list_models' + method: 'GET' + endpoint: '/models' chat: - name: "chat_completions" - method: "POST" - endpoint: "/chat/completions" + name: 'chat_completions' + method: 'POST' + endpoint: '/chat/completions' cloudflare: - id: "cloudflare" - url: "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai" - auth_type: "bearer" + id: 'cloudflare' + url: 'https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai' + auth_type: 'bearer' endpoints: models: - name: "list_models" - method: "GET" - endpoint: "/finetunes/public?limit=1000" + name: 'list_models' + method: 'GET' + endpoint: '/finetunes/public?limit=1000' chat: - name: "chat_completions" - method: "POST" - endpoint: "/v1/chat/completions" + name: 'chat_completions' + method: 'POST' + endpoint: '/v1/chat/completions' deepseek: - id: "deepseek" - url: "https://api.deepseek.com" - auth_type: "bearer" + id: 'deepseek' + url: 'https://api.deepseek.com' + auth_type: 'bearer' endpoints: models: - name: "list_models" - method: "GET" - endpoint: "/models" + name: 'list_models' + method: 'GET' + endpoint: '/models' chat: - name: "chat_completions" - method: "POST" - endpoint: "/chat/completions" + name: 'chat_completions' + method: 'POST' + endpoint: '/chat/completions' ProviderSpecificResponse: type: object description: | @@ -624,13 +624,13 @@ components: description: Message structure for provider requests properties: role: - $ref: "#/components/schemas/MessageRole" + $ref: '#/components/schemas/MessageRole' content: type: string tool_calls: type: array items: - $ref: "#/components/schemas/ChatCompletionMessageToolCall" + $ref: '#/components/schemas/ChatCompletionMessageToolCall' tool_call_id: type: string reasoning_content: @@ -656,7 +656,7 @@ components: owned_by: type: string served_by: - $ref: "#/components/schemas/Provider" + $ref: '#/components/schemas/Provider' required: - id - object @@ -668,13 +668,13 @@ components: description: Response structure for listing models properties: provider: - $ref: "#/components/schemas/Provider" + $ref: '#/components/schemas/Provider' object: type: string data: type: array items: - $ref: "#/components/schemas/Model" + $ref: '#/components/schemas/Model' default: [] required: - object @@ -686,11 +686,11 @@ components: object: type: string description: Always "list" - example: "list" + example: 'list' data: type: array items: - $ref: "#/components/schemas/MCPTool" + $ref: '#/components/schemas/MCPTool' default: [] description: Array of available MCP tools required: @@ -703,25 +703,25 @@ components: name: type: string description: The name of the tool - example: "read_file" + example: 'read_file' description: type: string description: A description of what the tool does - example: "Read content from a file" + example: 'Read content from a file' server: type: string description: The MCP server that provides this tool - example: "http://mcp-filesystem-server:8083/mcp" + example: 'http://mcp-filesystem-server:8083/mcp' input_schema: type: object description: JSON schema for the tool's input parameters example: - type: "object" + type: 'object' properties: file_path: - type: "string" - description: "Path to the file to read" - required: ["file_path"] + type: 'string' + description: 'Path to the file to read' + required: ['file_path'] additionalProperties: true required: - name @@ -741,7 +741,7 @@ components: The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. parameters: - $ref: "#/components/schemas/FunctionParameters" + $ref: '#/components/schemas/FunctionParameters' strict: type: boolean default: false @@ -758,9 +758,9 @@ components: type: object properties: type: - $ref: "#/components/schemas/ChatCompletionToolType" + $ref: '#/components/schemas/ChatCompletionToolType' function: - $ref: "#/components/schemas/FunctionObject" + $ref: '#/components/schemas/FunctionObject' required: - type - function @@ -831,7 +831,7 @@ components: type: array minItems: 1 items: - $ref: "#/components/schemas/Message" + $ref: '#/components/schemas/Message' max_tokens: description: > An upper bound for the number of tokens that can be generated @@ -845,7 +845,7 @@ components: type: boolean default: false stream_options: - $ref: "#/components/schemas/ChatCompletionStreamOptions" + $ref: '#/components/schemas/ChatCompletionStreamOptions' tools: type: array description: > @@ -854,7 +854,7 @@ components: the model may generate JSON inputs for. A max of 128 functions are supported. items: - $ref: "#/components/schemas/ChatCompletionTool" + $ref: '#/components/schemas/ChatCompletionTool' reasoning_format: type: string description: > @@ -891,9 +891,9 @@ components: type: string description: The ID of the tool call. type: - $ref: "#/components/schemas/ChatCompletionToolType" + $ref: '#/components/schemas/ChatCompletionToolType' function: - $ref: "#/components/schemas/ChatCompletionMessageToolCallFunction" + $ref: '#/components/schemas/ChatCompletionMessageToolCallFunction' required: - id - type @@ -925,7 +925,7 @@ components: type: integer description: The index of the choice in the list of choices. message: - $ref: "#/components/schemas/Message" + $ref: '#/components/schemas/Message' required: - finish_reason - index @@ -939,7 +939,7 @@ components: - index properties: delta: - $ref: "#/components/schemas/ChatCompletionStreamResponseDelta" + $ref: '#/components/schemas/ChatCompletionStreamResponseDelta' logprobs: description: Log probability information for the choice. type: object @@ -948,17 +948,17 @@ components: description: A list of message content tokens with log probability information. type: array items: - $ref: "#/components/schemas/ChatCompletionTokenLogprob" + $ref: '#/components/schemas/ChatCompletionTokenLogprob' refusal: description: A list of message refusal tokens with log probability information. type: array items: - $ref: "#/components/schemas/ChatCompletionTokenLogprob" + $ref: '#/components/schemas/ChatCompletionTokenLogprob' required: - content - refusal finish_reason: - $ref: "#/components/schemas/FinishReason" + $ref: '#/components/schemas/FinishReason' index: type: integer description: The index of the choice in the list of choices. @@ -977,7 +977,7 @@ components: A list of chat completion choices. Can be more than one if `n` is greater than 1. items: - $ref: "#/components/schemas/ChatCompletionChoice" + $ref: '#/components/schemas/ChatCompletionChoice' created: type: integer description: @@ -991,7 +991,7 @@ components: description: The object type, which is always `chat.completion`. x-stainless-const: true usage: - $ref: "#/components/schemas/CompletionUsage" + $ref: '#/components/schemas/CompletionUsage' required: - choices - created @@ -1014,9 +1014,9 @@ components: tool_calls: type: array items: - $ref: "#/components/schemas/ChatCompletionMessageToolCallChunk" + $ref: '#/components/schemas/ChatCompletionMessageToolCallChunk' role: - $ref: "#/components/schemas/MessageRole" + $ref: '#/components/schemas/MessageRole' refusal: type: string description: The refusal message generated by the model. @@ -1132,7 +1132,7 @@ components: last chunk if you set `stream_options: {"include_usage": true}`. items: - $ref: "#/components/schemas/ChatCompletionStreamChoice" + $ref: '#/components/schemas/ChatCompletionStreamChoice' created: type: integer description: @@ -1154,7 +1154,7 @@ components: type: string description: The object type, which is always `chat.completion.chunk`. usage: - $ref: "#/components/schemas/CompletionUsage" + $ref: '#/components/schemas/CompletionUsage' reasoning_format: type: string description: > @@ -1172,223 +1172,223 @@ components: x-config: sections: - general: - title: "General settings" + title: 'General settings' settings: - name: environment - env: "ENVIRONMENT" + env: 'ENVIRONMENT' type: string - default: "production" - description: "The environment" + default: 'production' + description: 'The environment' - name: enable_telemetry - env: "ENABLE_TELEMETRY" + env: 'ENABLE_TELEMETRY' type: bool - default: "false" - description: "Enable telemetry" + default: 'false' + description: 'Enable telemetry' - name: enable_auth - env: "ENABLE_AUTH" + env: 'ENABLE_AUTH' type: bool - default: "false" - description: "Enable authentication" + default: 'false' + description: 'Enable authentication' - mcp: - title: "Model Context Protocol (MCP)" + title: 'Model Context Protocol (MCP)' settings: - name: mcp_enable - env: "MCP_ENABLE" + env: 'MCP_ENABLE' type: bool - default: "false" - description: "Enable MCP" + default: 'false' + description: 'Enable MCP' - name: mcp_expose - env: "MCP_EXPOSE" + env: 'MCP_EXPOSE' type: bool - default: "false" - description: "Expose MCP tools endpoint" + default: 'false' + description: 'Expose MCP tools endpoint' - name: mcp_servers - env: "MCP_SERVERS" + env: 'MCP_SERVERS' type: string - description: "List of MCP servers" + description: 'List of MCP servers' - name: mcp_client_timeout - env: "MCP_CLIENT_TIMEOUT" + env: 'MCP_CLIENT_TIMEOUT' type: time.Duration - default: "5s" - description: "MCP client HTTP timeout" + default: '5s' + description: 'MCP client HTTP timeout' - name: mcp_dial_timeout - env: "MCP_DIAL_TIMEOUT" + env: 'MCP_DIAL_TIMEOUT' type: time.Duration - default: "3s" - description: "MCP client dial timeout" + default: '3s' + description: 'MCP client dial timeout' - name: mcp_tls_handshake_timeout - env: "MCP_TLS_HANDSHAKE_TIMEOUT" + env: 'MCP_TLS_HANDSHAKE_TIMEOUT' type: time.Duration - default: "3s" - description: "MCP client TLS handshake timeout" + default: '3s' + description: 'MCP client TLS handshake timeout' - name: mcp_response_header_timeout - env: "MCP_RESPONSE_HEADER_TIMEOUT" + env: 'MCP_RESPONSE_HEADER_TIMEOUT' type: time.Duration - default: "3s" - description: "MCP client response header timeout" + default: '3s' + description: 'MCP client response header timeout' - name: mcp_expect_continue_timeout - env: "MCP_EXPECT_CONTINUE_TIMEOUT" + env: 'MCP_EXPECT_CONTINUE_TIMEOUT' type: time.Duration - default: "1s" - description: "MCP client expect continue timeout" + default: '1s' + description: 'MCP client expect continue timeout' - name: mcp_request_timeout - env: "MCP_REQUEST_TIMEOUT" + env: 'MCP_REQUEST_TIMEOUT' type: time.Duration - default: "5s" - description: "MCP client request timeout for initialize and tool calls" + default: '5s' + description: 'MCP client request timeout for initialize and tool calls' - oidc: - title: "OpenID Connect" + title: 'OpenID Connect' settings: - name: issuer_url - env: "OIDC_ISSUER_URL" + env: 'OIDC_ISSUER_URL' type: string - default: "http://keycloak:8080/realms/inference-gateway-realm" - description: "OIDC issuer URL" + default: 'http://keycloak:8080/realms/inference-gateway-realm' + description: 'OIDC issuer URL' - name: client_id - env: "OIDC_CLIENT_ID" + env: 'OIDC_CLIENT_ID' type: string - default: "inference-gateway-client" - description: "OIDC client ID" + default: 'inference-gateway-client' + description: 'OIDC client ID' secret: true - name: client_secret - env: "OIDC_CLIENT_SECRET" + env: 'OIDC_CLIENT_SECRET' type: string - description: "OIDC client secret" + description: 'OIDC client secret' secret: true - server: - title: "Server settings" + title: 'Server settings' settings: - name: host - env: "SERVER_HOST" + env: 'SERVER_HOST' type: string - default: "0.0.0.0" - description: "Server host" + default: '0.0.0.0' + description: 'Server host' - name: port - env: "SERVER_PORT" + env: 'SERVER_PORT' type: string - default: "8080" - description: "Server port" + default: '8080' + description: 'Server port' - name: read_timeout - env: "SERVER_READ_TIMEOUT" + env: 'SERVER_READ_TIMEOUT' type: time.Duration - default: "30s" - description: "Read timeout" + default: '30s' + description: 'Read timeout' - name: write_timeout - env: "SERVER_WRITE_TIMEOUT" + env: 'SERVER_WRITE_TIMEOUT' type: time.Duration - default: "30s" - description: "Write timeout" + default: '30s' + description: 'Write timeout' - name: idle_timeout - env: "SERVER_IDLE_TIMEOUT" + env: 'SERVER_IDLE_TIMEOUT' type: time.Duration - default: "120s" - description: "Idle timeout" + default: '120s' + description: 'Idle timeout' - name: tls_cert_path - env: "SERVER_TLS_CERT_PATH" + env: 'SERVER_TLS_CERT_PATH' type: string - description: "TLS certificate path" + description: 'TLS certificate path' - name: tls_key_path - env: "SERVER_TLS_KEY_PATH" + env: 'SERVER_TLS_KEY_PATH' type: string - description: "TLS key path" + description: 'TLS key path' - client: - title: "Client settings" + title: 'Client settings' settings: - name: timeout - env: "CLIENT_TIMEOUT" + env: 'CLIENT_TIMEOUT' type: time.Duration - default: "30s" - description: "Client timeout" + default: '30s' + description: 'Client timeout' - name: max_idle_conns - env: "CLIENT_MAX_IDLE_CONNS" + env: 'CLIENT_MAX_IDLE_CONNS' type: int - default: "20" - description: "Maximum idle connections" + default: '20' + description: 'Maximum idle connections' - name: max_idle_conns_per_host - env: "CLIENT_MAX_IDLE_CONNS_PER_HOST" + env: 'CLIENT_MAX_IDLE_CONNS_PER_HOST' type: int - default: "20" - description: "Maximum idle connections per host" + default: '20' + description: 'Maximum idle connections per host' - name: idle_conn_timeout - env: "CLIENT_IDLE_CONN_TIMEOUT" + env: 'CLIENT_IDLE_CONN_TIMEOUT' type: time.Duration - default: "30s" - description: "Idle connection timeout" + default: '30s' + description: 'Idle connection timeout' - name: tls_min_version - env: "CLIENT_TLS_MIN_VERSION" + env: 'CLIENT_TLS_MIN_VERSION' type: string - default: "TLS12" - description: "Minimum TLS version" + default: 'TLS12' + description: 'Minimum TLS version' - providers: - title: "Providers" + title: 'Providers' settings: - name: anthropic_api_url - env: "ANTHROPIC_API_URL" + env: 'ANTHROPIC_API_URL' type: string - default: "https://api.anthropic.com/v1" - description: "Anthropic API URL" + default: 'https://api.anthropic.com/v1' + description: 'Anthropic API URL' - name: anthropic_api_key - env: "ANTHROPIC_API_KEY" + env: 'ANTHROPIC_API_KEY' type: string - description: "Anthropic API Key" + description: 'Anthropic API Key' secret: true - name: cloudflare_api_url - env: "CLOUDFLARE_API_URL" + env: 'CLOUDFLARE_API_URL' type: string - default: "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai" - description: "Cloudflare API URL" + default: 'https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai' + description: 'Cloudflare API URL' - name: cloudflare_api_key - env: "CLOUDFLARE_API_KEY" + env: 'CLOUDFLARE_API_KEY' type: string - description: "Cloudflare API Key" + description: 'Cloudflare API Key' secret: true - name: cohere_api_url - env: "COHERE_API_URL" + env: 'COHERE_API_URL' type: string - default: "https://api.cohere.ai" - description: "Cohere API URL" + default: 'https://api.cohere.ai' + description: 'Cohere API URL' - name: cohere_api_key - env: "COHERE_API_KEY" + env: 'COHERE_API_KEY' type: string - description: "Cohere API Key" + description: 'Cohere API Key' secret: true - name: groq_api_url - env: "GROQ_API_URL" + env: 'GROQ_API_URL' type: string - default: "https://api.groq.com/openai/v1" - description: "Groq API URL" + default: 'https://api.groq.com/openai/v1' + description: 'Groq API URL' - name: groq_api_key - env: "GROQ_API_KEY" + env: 'GROQ_API_KEY' type: string - description: "Groq API Key" + description: 'Groq API Key' secret: true - name: ollama_api_url - env: "OLLAMA_API_URL" + env: 'OLLAMA_API_URL' type: string - default: "http://ollama:8080/v1" - description: "Ollama API URL" + default: 'http://ollama:8080/v1' + description: 'Ollama API URL' - name: ollama_api_key - env: "OLLAMA_API_KEY" + env: 'OLLAMA_API_KEY' type: string - description: "Ollama API Key" + description: 'Ollama API Key' secret: true - name: openai_api_url - env: "OPENAI_API_URL" + env: 'OPENAI_API_URL' type: string - default: "https://api.openai.com/v1" - description: "OpenAI API URL" + default: 'https://api.openai.com/v1' + description: 'OpenAI API URL' - name: openai_api_key - env: "OPENAI_API_KEY" + env: 'OPENAI_API_KEY' type: string - description: "OpenAI API Key" + description: 'OpenAI API Key' secret: true - name: deepseek_api_url - env: "DEEPSEEK_API_URL" + env: 'DEEPSEEK_API_URL' type: string - default: "https://api.deepseek.com" - description: "DeepSeek API URL" + default: 'https://api.deepseek.com' + description: 'DeepSeek API URL' - name: deepseek_api_key - env: "DEEPSEEK_API_KEY" + env: 'DEEPSEEK_API_KEY' type: string - description: "DeepSeek API Key" + description: 'DeepSeek API Key' secret: true From f8bbb25fff628d494d9431e8b6730b97d2e61be3 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Tue, 27 May 2025 20:23:48 +0000 Subject: [PATCH 06/38] docs(examples): Update LLM model references to remove provider prefix Signed-off-by: Eden Reich --- examples/QUICKSTART.md | 6 +++--- examples/README.md | 4 ++-- examples/chat/README.md | 2 +- examples/mcp/README.md | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/QUICKSTART.md b/examples/QUICKSTART.md index d72bffb..2680e99 100644 --- a/examples/QUICKSTART.md +++ b/examples/QUICKSTART.md @@ -56,7 +56,7 @@ npm start ```bash cd chat export PROVIDER=groq -export LLM=groq/meta-llama/llama-3.3-70b-versatile +export LLM=meta-llama/llama-3.3-70b-versatile npm install npm start ``` @@ -66,7 +66,7 @@ npm start ```bash cd mcp export PROVIDER=groq -export LLM=groq/meta-llama/llama-3.3-70b-versatile +export LLM=meta-llama/llama-3.3-70b-versatile npm install npm start ``` @@ -77,7 +77,7 @@ npm start ```bash export PROVIDER=groq -export LLM=groq/meta-llama/llama-3.3-70b-versatile +export LLM=meta-llama/llama-3.3-70b-versatile ``` ### OpenAI (High quality) diff --git a/examples/README.md b/examples/README.md index db4b598..359cea6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,7 +20,7 @@ You should have docker installed or use the dev container in VS Code which has a ```bash export PROVIDER=groq - export LLM=groq/meta-llama/llama-4-maverick-17b-128e-instruct + export LLM=meta-llama/llama-4-maverick-17b-128e-instruct ``` 4. Review the different examples in the specific directories: @@ -93,7 +93,7 @@ npm start All examples support these environment variables: - `PROVIDER` - AI provider to use (groq, openai, anthropic, etc.) -- `LLM` - Specific model to use (e.g., groq/meta-llama/llama-3.3-70b-versatile) +- `LLM` - Specific model to use (e.g., meta-llama/llama-3.3-70b-versatile) Provider-specific API keys should be set in the `.env` file (see `.env.example`). diff --git a/examples/chat/README.md b/examples/chat/README.md index e28c3bf..34d3187 100644 --- a/examples/chat/README.md +++ b/examples/chat/README.md @@ -23,7 +23,7 @@ This example demonstrates how to use the Inference Gateway SDK for chat applicat ```bash export PROVIDER=groq - export LLM=groq/meta-llama/llama-3.3-70b-versatile + export LLM=meta-llama/llama-3.3-70b-versatile ``` Or for OpenAI: diff --git a/examples/mcp/README.md b/examples/mcp/README.md index 5e9c87d..88282dd 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -68,7 +68,7 @@ Set your preferred provider and model: ```bash export PROVIDER=groq -export LLM=groq/meta-llama/llama-3.3-70b-versatile +export LLM=meta-llama/llama-3.3-70b-versatile ``` Or for OpenAI: From 16fa1f2e6d176bd85b092b8b77b8bb12983e27de Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Tue, 27 May 2025 20:24:43 +0000 Subject: [PATCH 07/38] docs(mcp): Add note to ensure no containers are running before starting the example Signed-off-by: Eden Reich --- examples/mcp/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/mcp/README.md b/examples/mcp/README.md index 88282dd..a979699 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -2,6 +2,8 @@ This example demonstrates how to use the Inference Gateway SDK with Model Context Protocol (MCP) tools in a multi-provider architecture. It showcases how to connect to MCP servers, discover available tools, and use them in AI conversations. +Please ensure you have no containers running before starting this example, as it uses Docker Compose to set up the necessary infrastructure. + ## Features Demonstrated 1. **MCP Tool Discovery** - List and explore available MCP tools From 9e66ef5b630a48eda88bc3a7b06d3b4addf8a6e9 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Wed, 28 May 2025 22:23:18 +0000 Subject: [PATCH 08/38] feat: Support both local tools and remote MCP tools; Add onMCPTool callback - Created a new MCP Web Search Server with a package.json file. - Updated the main MCP package.json to include example scripts for remote and local tools. - Added a .gitignore file in the shared directory to exclude unnecessary files. - Removed outdated README.md, config.yaml, data.json, and sample.txt files from the shared directory. - Introduced a new sample_sales_data.csv file for demonstration purposes. - Enhanced the client.ts to support MCP tool callbacks and improved streaming response processing. Signed-off-by: Eden Reich --- .gitattributes | 3 + examples/README.md | 2 +- examples/mcp/README.md | 337 ++- examples/mcp/docker-compose.yml | 193 +- examples/mcp/example-mcp-tools.ts | 176 ++ examples/mcp/example-mcp-with-tools.ts | 0 examples/mcp/index.ts | 196 +- examples/mcp/mcp-servers/README.md | 241 +++ examples/mcp/mcp-servers/filesystem/README.md | 200 ++ examples/mcp/mcp-servers/filesystem/index.js | 621 ++++++ .../mcp-servers/filesystem/package-lock.json | 1433 +++++++++++++ .../mcp/mcp-servers/filesystem/package.json | 22 + examples/mcp/mcp-servers/web-search/README.md | 130 ++ .../mcp/mcp-servers/web-search/index-http.js | 331 +++ examples/mcp/mcp-servers/web-search/index.js | 363 ++++ .../mcp-servers/web-search/package-lock.json | 1798 +++++++++++++++++ .../mcp/mcp-servers/web-search/package.json | 24 + examples/mcp/package.json | 2 + examples/mcp/shared/.gitignore | 3 + examples/mcp/shared/README.md | 13 - examples/mcp/shared/config.yaml | 49 - examples/mcp/shared/data.json | 42 - examples/mcp/shared/sample.txt | 18 - examples/mcp/shared/sample_sales_data.csv | 16 + src/client.ts | 413 ++-- 25 files changed, 6129 insertions(+), 497 deletions(-) create mode 100644 examples/mcp/example-mcp-tools.ts create mode 100644 examples/mcp/example-mcp-with-tools.ts create mode 100644 examples/mcp/mcp-servers/README.md create mode 100644 examples/mcp/mcp-servers/filesystem/README.md create mode 100644 examples/mcp/mcp-servers/filesystem/index.js create mode 100644 examples/mcp/mcp-servers/filesystem/package-lock.json create mode 100644 examples/mcp/mcp-servers/filesystem/package.json create mode 100644 examples/mcp/mcp-servers/web-search/README.md create mode 100644 examples/mcp/mcp-servers/web-search/index-http.js create mode 100644 examples/mcp/mcp-servers/web-search/index.js create mode 100644 examples/mcp/mcp-servers/web-search/package-lock.json create mode 100644 examples/mcp/mcp-servers/web-search/package.json create mode 100644 examples/mcp/shared/.gitignore delete mode 100644 examples/mcp/shared/README.md delete mode 100644 examples/mcp/shared/config.yaml delete mode 100644 examples/mcp/shared/data.json delete mode 100644 examples/mcp/shared/sample.txt create mode 100644 examples/mcp/shared/sample_sales_data.csv diff --git a/.gitattributes b/.gitattributes index 3d4500a..2d3f664 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ .devcontainer/** linguist-vendored=true +src/examples/** linguist-vendored=true + src/types/generated/** linguist-generated=true + diff --git a/examples/README.md b/examples/README.md index 359cea6..5e70c5f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,7 +20,7 @@ You should have docker installed or use the dev container in VS Code which has a ```bash export PROVIDER=groq - export LLM=meta-llama/llama-4-maverick-17b-128e-instruct + export LLM=llama-3.3-70b-versatile ``` 4. Review the different examples in the specific directories: diff --git a/examples/mcp/README.md b/examples/mcp/README.md index a979699..cd134b9 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -11,16 +11,34 @@ Please ensure you have no containers running before starting this example, as it 3. **Web Scraping** - Fetch content from URLs using MCP tools 4. **Multi-Tool Conversations** - Combine multiple MCP tools in single conversations 5. **Tool Function Calling** - Stream responses with real-time tool execution +6. **Data Analysis** - Analyze sample data files with AI assistance ## Architecture This example uses Docker Compose to orchestrate: - **Inference Gateway** - Main API gateway with MCP support enabled -- **MCP Filesystem Server** - Provides file system operations +- **MCP Filesystem Server** - Provides file system operations (restricted to `/shared` and `/tmp`) - **MCP Web Search Server** - Simulated web search and URL fetching - **Optional Ollama** - Local model inference (when using `--profile with-ollama`) +## Important: Filesystem Access + +The MCP filesystem server is configured with restricted access for security: + +- **`/shared`** - Read-only directory with sample data files +- **`/tmp`** - Read-write directory for temporary files + +The AI will only be able to access these directories. This prevents unauthorized access to system files. + +## Sample Data + +The `/shared` directory contains example files for testing: + +- `mcp-filesystem-example.txt` - Basic example file +- `sample_sales_data.csv` - Sales data for analysis exercises +- `README.md` - Documentation about available files + ## Getting Started ### Prerequisites @@ -28,23 +46,13 @@ This example uses Docker Compose to orchestrate: - Docker and Docker Compose installed - API key for at least one provider (OpenAI, Groq, Anthropic, etc.) -### 1. Environment Setup - -Copy the parent `.env.example` to `.env` and configure your API keys: +Make sure the environment is configured: ```bash -cp ../.env.example ../.env +cp .env.example .env ``` -Edit `../.env` and add your API keys: - -```bash -GROQ_API_KEY=your_groq_api_key_here -OPENAI_API_KEY=your_openai_api_key_here -ANTHROPIC_API_KEY=your_anthropic_api_key_here -``` - -### 2. Start the MCP Infrastructure +### 1. Start the MCP Infrastructure Start all services using Docker Compose: @@ -58,13 +66,13 @@ This will start: - MCP Filesystem server on port 3000 - MCP Web Search server on port 3001 -### 3. Install Dependencies +### 2. Install Dependencies ```bash npm install ``` -### 4. Configure Provider and Model +### 3. Configure Provider and Model Set your preferred provider and model: @@ -80,19 +88,69 @@ export PROVIDER=openai export LLM=gpt-4o ``` -### 5. Run the Example +### 4. Verify MCP Setup + +Test that MCP tools are working correctly: + +```bash +npx tsx test-mcp-tools.ts +``` + +### 5. Run Examples + +Run the main example: ```bash npm start ``` +Or run the focused filesystem demo: + +```bash +npx tsx filesystem-demo.ts +``` + +## Available Examples + +- `index.ts` - Complete MCP example with multiple scenarios +- `filesystem-demo.ts` - Focused demonstration of filesystem operations +- `test-mcp-tools.ts` - Simple verification that MCP tools are working +- `advanced-example.ts` - More complex MCP usage patterns + ## Available Commands -- `npm start` - Run the MCP example +- `npm start` - Run the main MCP example - `npm run compose:up` - Start all services in background - `npm run compose:down` - Stop all services - `npm run compose:logs` - View logs from all services +## Example Prompts to Try + +Once the example is running, you can ask the AI: + +1. **List available data:** + + ``` + "Can you show me what files are available in the /shared directory?" + ``` + +2. **Analyze sample data:** + + ``` + "Read the sales data from /shared/sample_sales_data.csv and give me a summary of the top-selling products" + ``` + +3. **Create reports:** + + ``` + "Based on the sales data, create a summary report and save it to /tmp/sales_report.txt" + ``` + +4. **File operations:** + ``` + "Create a todo list with 5 tasks and save it to /tmp/todo.txt, then read it back to me" + ``` + ## Example Output The example will demonstrate: @@ -104,7 +162,7 @@ The example will demonstrate: Gateway health: ✅ Healthy 📋 Listing available MCP tools... -Found 4 MCP tools: +Found 9 MCP tools: 1. read_file Description: Read content from a file @@ -123,7 +181,56 @@ Found 4 MCP tools: 2. write_file Description: Write content to a file Server: http://mcp-filesystem:3000/mcp - ... + Schema: { + "type": "object", + "properties": { + "file_path": { + "type": "string", + "description": "Path to the file to write" + }, + "content": { + "type": "string", + "description": "Content to write to the file" + } + }, + "required": ["file_path", "content"] + } + +3. fetch_url + Description: Fetch content from a URL + Server: http://mcp-web-search:3001/mcp + Schema: { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The URL to fetch content from" + }, + "timeout": { + "type": "number", + "description": "Request timeout in milliseconds" + } + }, + "required": ["url"] + } + +4. search_web + Description: Search the web and return results + Server: http://mcp-web-search:3001/mcp + Schema: { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query" + }, + "limit": { + "type": "number", + "description": "Number of results to return" + } + }, + "required": ["query"] + } === Example 1: File Operations with MCP === @@ -210,7 +317,7 @@ If you see "No MCP tools available": 3. Ensure the Inference Gateway can reach MCP servers: ```bash - docker exec inference-gateway-mcp curl -f http://mcp-filesystem:3000/mcp + curl -X POST http://localhost:8080/mcp/tools/list -H "Content-Type: application/json" -d '{}' ``` ### Gateway Health Check Fails @@ -234,10 +341,12 @@ If the gateway appears unhealthy: If tool calls fail during conversations: 1. Verify the model supports tool calling (GPT-4, Claude, etc.) -2. Check MCP server responses: +2. Check MCP server connectivity through the gateway: ```bash - curl -X POST http://localhost:3000/mcp/tools/list + curl -X POST http://localhost:8080/mcp/tools/list \ + -H "Content-Type: application/json" \ + -d '{}' ``` 3. Enable debug logging by adding to docker-compose.yml: @@ -264,7 +373,7 @@ To add your own MCP server: 2. Update Inference Gateway environment: ```yaml - MCP_SERVERS: 'filesystem=http://mcp-filesystem:3000/mcp,web-search=http://mcp-web-search:3001/mcp,custom=http://my-custom-mcp:3002/mcp' + MCP_SERVERS: 'http://mcp-filesystem:3000/mcp,http://mcp-web-search:3001/mcp,http://my-custom-mcp:3002/mcp' ``` 3. Restart services: @@ -287,3 +396,183 @@ To add your own MCP server: - [Model Context Protocol Specification](https://modelcontextprotocol.io/) - [Inference Gateway Documentation](https://github.com/inference-gateway/inference-gateway) - [Official MCP Servers](https://github.com/modelcontextprotocol/servers) + +## HTTP Examples + +This section provides practical HTTP examples for interacting with MCP tools through the Inference Gateway. These examples are useful for testing, debugging, or integrating with other systems. **Note: Always interact with MCP tools through the Inference Gateway, not directly with MCP servers.** + +### Prerequisites for HTTP Examples + +Make sure the MCP infrastructure is running with MCP tools exposed: + +```bash +npm run compose:up +``` + +**Important:** Ensure the Inference Gateway is started with `EXPOSE_MCP=true` environment variable to enable MCP endpoints. + +### 1. Health Checks + +#### Check Inference Gateway Health + +```bash +curl -X GET http://localhost:8080/health +``` + +**Response:** + +```json +{ + "status": "healthy", + "timestamp": "2025-05-28T10:30:00Z", + "version": "1.0.0" +} +``` + +### 2. MCP Tool Discovery + +#### List All Available MCP Tools via Inference Gateway + +```bash +curl -X GET http://localhost:8080/v1/mcp/tools +``` + +**Response:** + +```json +{ + "tools": [ + { + "name": "read_file", + "description": "Read content from a file", + "input_schema": { + "type": "object", + "properties": { + "file_path": { + "type": "string", + "description": "Path to the file to read" + } + }, + "required": ["file_path"] + } + }, + { + "name": "write_file", + "description": "Write content to a file", + "input_schema": { + "type": "object", + "properties": { + "file_path": { + "type": "string", + "description": "Path to the file to write" + }, + "content": { + "type": "string", + "description": "Content to write to the file" + } + }, + "required": ["file_path", "content"] + } + }, + { + "name": "fetch_url", + "description": "Fetch content from a URL", + "input_schema": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The URL to fetch content from" + }, + "timeout": { + "type": "number", + "description": "Request timeout in milliseconds" + } + }, + "required": ["url"] + } + } + ] +} +``` + +### 3. Using MCP Tools in Chat Completions + +MCP tools are executed automatically by AI models during chat completions. You don't call them directly - instead, you include them in the `tools` array of a chat completion request. + +#### Basic Chat Completion with MCP Tools + +```bash +curl -X POST http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "groq/llama-3.3-70b-versatile", + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant that can read files using available tools." + }, + { + "role": "user", + "content": "Can you read the contents of /tmp/example.txt?" + } + ] + }' +``` + +#### Web Scraping with MCP Tools + +```bash +curl -X POST http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "openai/gpt-4o", + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant that can fetch web content." + }, + { + "role": "user", + "content": "Can you fetch the content from https://httpbin.org/json?" + } + ] + }' +``` + +### 4. Streaming Chat Completions with MCP Tools + +```bash +curl -X POST http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "anthropic/claude-3-haiku-20240307", + "messages": [ + { + "role": "user", + "content": "Help me analyze the file /shared/data.csv by reading it first" + } + ], + "stream": true + }' +``` + +### 5. Multi-Tool Conversations + +```bash +curl -X POST http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "groq/mixtral-8x7b-32768", + "messages": [ + { + "role": "system", + "content": "You are a research assistant with access to file operations and web content fetching." + }, + { + "role": "user", + "content": "Research information about AI from https://openai.com and save a summary to /tmp/ai-research.txt" + } + ] + }' +``` diff --git a/examples/mcp/docker-compose.yml b/examples/mcp/docker-compose.yml index fd9f3e6..1db24fe 100644 --- a/examples/mcp/docker-compose.yml +++ b/examples/mcp/docker-compose.yml @@ -1,16 +1,16 @@ -version: '3.8' - services: - # Inference Gateway inference-gateway: image: ghcr.io/inference-gateway/inference-gateway:latest ports: - '8080:8080' environment: + # General settings + ENVIRONMENT: development + # Enable MCP support MCP_ENABLE: 'true' MCP_EXPOSE: 'true' - MCP_SERVERS: 'filesystem=http://mcp-filesystem:3000/mcp,web-search=http://mcp-web-search:3001/mcp' + MCP_SERVERS: 'http://mcp-filesystem:3000/mcp,http://mcp-web-search:3001/mcp' # Server settings SERVER_HOST: '0.0.0.0' @@ -26,12 +26,17 @@ services: ANTHROPIC_API_URL: 'https://api.anthropic.com/v1' ANTHROPIC_API_KEY: '${ANTHROPIC_API_KEY:-}' + DEEPSEEK_API_URL: 'https://api.deepseek.com/v1' + DEEPSEEK_API_KEY: '${DEEPSEEK_API_KEY:-}' + # Optional: Ollama for local models OLLAMA_API_URL: 'http://ollama:11434/v1' OLLAMA_API_KEY: '' depends_on: - - mcp-filesystem - - mcp-web-search + mcp-filesystem: + condition: service_healthy + mcp-web-search: + condition: service_healthy networks: - inference-network volumes: @@ -40,153 +45,66 @@ services: test: ['CMD', 'curl', '-f', 'http://localhost:8080/health'] interval: 30s timeout: 10s - retries: 3 + retries: 5 + start_period: 60s + pull_policy: always + restart: unless-stopped - # MCP Filesystem Server mcp-filesystem: - image: ghcr.io/modelcontextprotocol/servers/filesystem:latest - ports: - - '3000:3000' + build: + context: ./mcp-servers/filesystem + dockerfile_inline: | + FROM node:18-alpine + WORKDIR /app + RUN apk add --no-cache curl + COPY package.json ./ + RUN npm install + COPY . . + EXPOSE 3000 + CMD ["npm", "start"] environment: MCP_SERVER_NAME: 'filesystem' MCP_SERVER_VERSION: '1.0.0' - ALLOWED_DIRECTORIES: '/shared,/tmp' + ALLOWED_DIRECTORIES: '/tmp' + NODE_ENV: 'production' networks: - inference-network volumes: - shared-data:/shared - - ./mcp-data:/tmp - command: > - sh -c " - mkdir -p /tmp && - echo 'Hello from MCP filesystem server!' > /tmp/example.txt && - echo 'This is a sample file for testing MCP file operations.' >> /tmp/example.txt && - echo 'Created at: $(date)' >> /tmp/example.txt && - mcp-server-filesystem --allowed-directories /shared,/tmp - " + - ./shared:/tmp + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:3000/health'] + interval: 30s + timeout: 10s + retries: 5 + start_period: 45s + restart: unless-stopped - # MCP Web Search Server (example using a hypothetical web search MCP server) mcp-web-search: - image: node:18-alpine - ports: - - '3001:3001' - working_dir: /app + build: + context: ./mcp-servers/web-search + dockerfile_inline: | + FROM node:18-alpine + WORKDIR /app + RUN apk add --no-cache curl + COPY package.json ./ + RUN npm install + COPY . . + EXPOSE 3001 + CMD ["node", "index-http.js"] environment: NODE_ENV: 'production' MCP_SERVER_NAME: 'web-search' MCP_SERVER_VERSION: '1.0.0' networks: - inference-network - volumes: - - ./mcp-servers/web-search:/app - command: > - sh -c " - if [ ! -f package.json ]; then - npm init -y && - npm install express cors axios cheerio && - cat > index.js << 'EOF' - const express = require('express'); - const cors = require('cors'); - const axios = require('axios'); - - const app = express(); - app.use(cors()); - app.use(express.json()); - - // MCP server info - app.get('/mcp', (req, res) => { - res.json({ - capabilities: { - tools: { - listChanged: false - } - }, - serverInfo: { - name: 'web-search', - version: '1.0.0' - } - }); - }); - - // List available tools - app.post('/mcp/tools/list', (req, res) => { - res.json({ - tools: [ - { - name: 'fetch_url', - description: 'Fetch content from a URL', - inputSchema: { - type: 'object', - properties: { - url: { - type: 'string', - description: 'The URL to fetch' - } - }, - required: ['url'] - } - }, - { - name: 'search_web', - description: 'Search the web for information', - inputSchema: { - type: 'object', - properties: { - query: { - type: 'string', - description: 'The search query' - } - }, - required: ['query'] - } - } - ] - }); - }); - - // Execute tools - app.post('/mcp/tools/call', async (req, res) => { - try { - const { name, arguments: args } = req.body; - - if (name === 'fetch_url') { - const response = await axios.get(args.url, { timeout: 10000 }); - res.json({ - content: [ - { - type: 'text', - text: typeof response.data === 'object' - ? JSON.stringify(response.data, null, 2) - : response.data.toString().substring(0, 5000) - } - ] - }); - } else if (name === 'search_web') { - // Simulate web search (in real implementation, you'd use a real search API) - res.json({ - content: [ - { - type: 'text', - text: \`Search results for \"\${args.query}\":\n\n1. Example result for \${args.query}\n2. Another relevant result\n3. More information about \${args.query}\` - } - ] - }); - } else { - res.status(400).json({ error: 'Unknown tool: ' + name }); - } - } catch (error) { - res.status(500).json({ error: error.message }); - } - }); - - const port = process.env.PORT || 3001; - app.listen(port, '0.0.0.0', () => { - console.log(\`MCP Web Search server running on port \${port}\`); - }); - EOF - fi && - node index.js - " + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:3001/health'] + interval: 30s + timeout: 10s + retries: 5 + start_period: 45s + restart: unless-stopped # Optional: Ollama for local models ollama: @@ -199,8 +117,7 @@ services: - ollama-data:/root/.ollama environment: OLLAMA_HOST: '0.0.0.0' - profiles: - - 'with-ollama' + restart: unless-stopped volumes: shared-data: diff --git a/examples/mcp/example-mcp-tools.ts b/examples/mcp/example-mcp-tools.ts new file mode 100644 index 0000000..0b0a03a --- /dev/null +++ b/examples/mcp/example-mcp-tools.ts @@ -0,0 +1,176 @@ +import { + InferenceGatewayClient, + MessageRole, + Provider, +} from '../../src/index.js'; + +/** + * Demonstration of MCP filesystem operations with proper directory access + * This example shows how to work with the /shared and /tmp directories + * and demonstrates the onMCPTool callback for tracking tool usage + */ +(async () => { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + const provider = (process.env.PROVIDER as Provider) || Provider.groq; + const model = process.env.LLM || 'llama-3.3-70b-versatile'; + + console.log(`Using model: ${model}`); + console.log(`Using provider: ${provider}\n`); + + console.log('=== MCP Tool Usage Demo ===\n'); + + try { + // Check gateway health + console.log('🔍 Checking gateway health...'); + const isHealthy = await client.healthCheck(); + console.log( + `Gateway health: ${isHealthy ? '✅ Healthy' : '❌ Unhealthy'}\n` + ); + + if (!isHealthy) { + console.log( + 'Please ensure the Inference Gateway is running with Docker Compose.' + ); + process.exit(1); + } + + // List available MCP tools + console.log('📋 Listing available MCP tools...'); + const tools = await client.listTools(); + console.log(`Found ${tools.data.length} MCP tools:\n`); + + const fileTools = tools.data.filter((tool) => + ['read_file', 'write_file', 'list_directory'].includes(tool.name) + ); + + if (fileTools.length === 0) { + console.log('⚠️ No filesystem MCP tools available.'); + return; + } + + console.log('📁 Available filesystem tools:'); + fileTools.forEach((tool, index) => { + console.log(`${index + 1}. ${tool.name} - ${tool.description}`); + }); + console.log(''); + + // Track MCP tool calls for demonstration + const toolCallTracker = { + totalCalls: 0, + toolsUsed: new Set(), + filesAccessed: new Set(), + }; + + // Example: Analyze highest revenue from sales data + console.log('=== Highest Revenue Analysis with MCP Tool Tracking ===\n'); + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: `You are a data analyst with filesystem access. You have access to tools that can read files from /tmp directory and write files to /tmp directory. When analyzing data, be thorough and provide specific insights.`, + }, + { + role: MessageRole.user, + content: + 'Please read the sample_sales_data.csv file from the /tmp directory, analyze it to find the highest revenue transactions, and create a detailed summary report. Save the summary to /tmp/revenue_analysis.txt.', + }, + ], + max_tokens: 1500, + }, + { + onOpen: () => { + console.log('🚀 Starting revenue analysis...'); + console.log('📊 MCP Tool usage will be tracked below:\n'); + }, + onContent: (content) => process.stdout.write(content), + onMCPTool: (toolCall) => { + toolCallTracker.totalCalls++; + toolCallTracker.toolsUsed.add(toolCall.function.name); + + console.log( + `\n🔧 [TOOL CALL #${toolCallTracker.totalCalls}] ${toolCall.function.name}` + ); + + const args = JSON.parse(toolCall.function.arguments); + + switch (toolCall.function.name) { + case 'read_file': + console.log(` 📄 Reading file: ${args.path}`); + toolCallTracker.filesAccessed.add(args.path); + break; + case 'write_file': + console.log(` 💾 Writing file: ${args.path}`); + console.log( + ` 📝 Content length: ${ + args.content ? args.content.length : 0 + } characters` + ); + toolCallTracker.filesAccessed.add(args.path); + break; + case 'list_directory': + console.log(` 📂 Listing directory: ${args.path}`); + break; + default: + console.log(` ⚙️ Arguments: ${JSON.stringify(args, null, 2)}`); + } + console.log(` 🆔 Tool ID: ${toolCall.id}`); + }, + onFinish: () => { + console.log('\n\n✅ Revenue analysis completed!\n'); + + // Display tool usage summary + console.log('📈 MCP Tool Usage Summary:'); + console.log(` Total tool calls: ${toolCallTracker.totalCalls}`); + console.log( + ` Tools used: ${Array.from(toolCallTracker.toolsUsed).join(', ')}` + ); + console.log( + ` Files accessed: ${Array.from( + toolCallTracker.filesAccessed + ).join(', ')}` + ); + console.log(''); + }, + onError: (error) => console.error('❌ Error:', error), + }, + provider + ); + + console.log('🎉 MCP Tool Usage Demo completed successfully!'); + console.log('\n💡 Key takeaways:'); + console.log( + '- The onMCPTool callback provides detailed tracking of tool usage' + ); + console.log( + '- Track total tool calls, tool types used, and files accessed' + ); + console.log( + '- Each tool call includes function name, arguments, and unique ID' + ); + console.log( + '- Perfect for debugging, monitoring, and understanding AI tool usage patterns' + ); + console.log( + '- LLM can read CSV data and perform complex analysis with file operations\n' + ); + } catch (error) { + if ( + error instanceof Error && + error.message.includes('MCP tools endpoint is not exposed') + ) { + console.error( + '❌ MCP tools are not exposed. Please ensure the Inference Gateway is started with MCP_EXPOSE=true' + ); + console.log('\n💡 To fix this, restart the gateway with:'); + console.log(' docker-compose up --build'); + } else { + console.error('❌ Error:', error); + } + } +})(); diff --git a/examples/mcp/example-mcp-with-tools.ts b/examples/mcp/example-mcp-with-tools.ts new file mode 100644 index 0000000..e69de29 diff --git a/examples/mcp/index.ts b/examples/mcp/index.ts index 3c6c37c..4b915c0 100644 --- a/examples/mcp/index.ts +++ b/examples/mcp/index.ts @@ -1,9 +1,8 @@ import { - ChatCompletionToolType, InferenceGatewayClient, MessageRole, Provider, -} from '@inference-gateway/sdk'; +} from '../../src/index.js'; (async () => { const client = new InferenceGatewayClient({ @@ -11,7 +10,10 @@ import { }); const provider = (process.env.PROVIDER as Provider) || Provider.groq; - const model = process.env.LLM || 'groq/meta-llama/llama-3.3-70b-versatile'; + const model = process.env.LLM || 'llama-3.3-70b-versatile'; + + console.log(`Using model: ${model}`); + console.log(`Using provider: ${provider}\n`); console.log('=== MCP Tools Example ===\n'); @@ -49,10 +51,79 @@ import { return; } - // Example 1: Use MCP tools for file operations (if filesystem MCP server is available) + // Example 0: Simple test without tools first + console.log('=== Example 0: Simple Test (No Tools) ===\n'); + console.log('Testing basic streaming without tools first...\n'); + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.user, + content: 'Hello! Please tell me about the weather.', + }, + ], + max_tokens: 50, + }, + { + onOpen: () => console.log('🚀 Starting simple test...'), + onContent: (content) => process.stdout.write(content), + onTool: (toolCall) => { + console.log(`\n🔧 Tool called: ${toolCall.function.name}`); + console.log(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onFinish: () => { + console.log('\n✅ Simple test completed\n'); + }, + onError: (error) => console.error('❌ Error:', error), + }, + provider + ); + + // Example 1: Automatic tool discovery and usage + console.log('=== Example 1: Automatic Tool Discovery ===\n'); + console.log( + 'The gateway automatically detects and uses available MCP tools based on context.\n' + ); + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: + 'You are a helpful assistant with access to various tools.', + }, + { + role: MessageRole.user, + content: + 'What time is it now? Also, if you can, find some information about artificial intelligence.', + }, + ], + max_tokens: 200, + }, + { + onOpen: () => console.log('🚀 Starting automatic tool discovery...'), + onContent: (content) => process.stdout.write(content), + onTool: (toolCall) => { + console.log( + `\n🔧 Tool automatically called: ${toolCall.function.name}` + ); + console.log(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onFinish: () => + console.log('\n✅ Automatic tool discovery completed\n'), + onError: (error) => console.error('❌ Error:', error), + }, + provider + ); + + // Example 2: Use MCP tools for file operations (if filesystem MCP server is available) const fileReadTool = tools.data.find((tool) => tool.name === 'read_file'); if (fileReadTool) { - console.log('=== Example 1: File Operations with MCP ===\n'); + console.log('=== Example 2: File Operations with MCP ===\n'); await client.streamChatCompletion( { @@ -61,25 +132,15 @@ import { { role: MessageRole.system, content: - 'You are a helpful assistant that can read files using MCP tools. When asked to read a file, use the read_file tool.', + 'You are a helpful assistant with access to filesystem operations. Available directories are /shared and /tmp.', }, { role: MessageRole.user, content: - 'Can you read the contents of /tmp/example.txt if it exists?', - }, - ], - tools: [ - { - type: ChatCompletionToolType.function, - function: { - name: fileReadTool.name, - description: fileReadTool.description, - parameters: fileReadTool.input_schema, - strict: true, - }, + 'Can you read the contents of /shared/mcp-filesystem-example.txt and tell me what it contains?', }, ], + max_tokens: 200, }, { onOpen: () => console.log('🚀 Starting file reading example...'), @@ -95,12 +156,12 @@ import { ); } - // Example 2: Use MCP tools for web scraping (if web scraper MCP server is available) + // Example 3: Use MCP tools for web scraping (if web scraper MCP server is available) const webScrapeTool = tools.data.find( (tool) => tool.name.includes('fetch') || tool.name.includes('scrape') ); if (webScrapeTool) { - console.log('=== Example 2: Web Scraping with MCP ===\n'); + console.log('=== Example 3: Web Scraping with MCP ===\n'); await client.streamChatCompletion( { @@ -109,7 +170,7 @@ import { { role: MessageRole.system, content: - 'You are a helpful assistant that can fetch web content using MCP tools. Use the available tools to gather information from websites.', + 'You are a helpful assistant with access to web search capabilities.', }, { role: MessageRole.user, @@ -117,17 +178,7 @@ import { 'Can you fetch information from https://httpbin.org/json and tell me what you find?', }, ], - tools: [ - { - type: ChatCompletionToolType.function, - function: { - name: webScrapeTool.name, - description: webScrapeTool.description, - parameters: webScrapeTool.input_schema, - strict: true, - }, - }, - ], + max_tokens: 200, }, { onOpen: () => console.log('🚀 Starting web scraping example...'), @@ -143,9 +194,9 @@ import { ); } - // Example 3: Generic MCP tool usage - use the first available tool + // Example 4: Generic MCP tool usage - use the first available tool if (tools.data.length > 0 && !fileReadTool && !webScrapeTool) { - console.log('=== Example 3: Generic MCP Tool Usage ===\n'); + console.log('=== Example 4: Generic MCP Tool Usage ===\n'); const firstTool = tools.data[0]; console.log(`Using tool: ${firstTool.name}\n`); @@ -156,24 +207,14 @@ import { messages: [ { role: MessageRole.system, - content: `You are a helpful assistant that has access to the ${firstTool.name} tool. Use it when appropriate to help the user.`, + content: `You are a helpful assistant with access to various tools including ${firstTool.name}.`, }, { role: MessageRole.user, content: `Can you help me use the ${firstTool.name} tool? What can it do?`, }, ], - tools: [ - { - type: ChatCompletionToolType.function, - function: { - name: firstTool.name, - description: firstTool.description, - parameters: firstTool.input_schema, - strict: true, - }, - }, - ], + max_tokens: 200, }, { onOpen: () => console.log('🚀 Starting generic tool example...'), @@ -189,19 +230,9 @@ import { ); } - // Example 4: Multi-tool conversation + // Example 5: Data Analysis with File Operations if (tools.data.length > 1) { - console.log('=== Example 4: Multi-Tool Conversation ===\n'); - - const availableTools = tools.data.slice(0, 3).map((tool) => ({ - type: ChatCompletionToolType.function, - function: { - name: tool.name, - description: tool.description, - parameters: tool.input_schema, - strict: true, - }, - })); + console.log('=== Example 5: Data Analysis with File Operations ===\n'); await client.streamChatCompletion( { @@ -209,35 +240,62 @@ import { messages: [ { role: MessageRole.system, - content: `You are a helpful assistant with access to multiple MCP tools: ${tools.data - .slice(0, 3) - .map((t) => t.name) - .join( - ', ' - )}. Use these tools to help the user accomplish their tasks.`, + content: `You are a helpful data analysis assistant with access to filesystem tools. Available directories are /shared and /tmp. You can read, write, and analyze files. The /shared directory contains sample data files for analysis.`, }, { role: MessageRole.user, content: - 'I need help with data analysis. Can you show me what tools are available and suggest how to use them?', + 'I need help with data analysis. First, can you check what files are available in the /shared directory? Then create a simple CSV file with sample sales data in /tmp/sales_data.csv and analyze it.', }, ], - tools: availableTools, + max_tokens: 400, }, { - onOpen: () => console.log('🚀 Starting multi-tool conversation...'), + onOpen: () => console.log('🚀 Starting data analysis example...'), onContent: (content) => process.stdout.write(content), onTool: (toolCall) => { console.log(`\n🔧 Tool called: ${toolCall.function.name}`); console.log(`📝 Arguments: ${toolCall.function.arguments}`); }, - onFinish: () => - console.log('\n✅ Multi-tool conversation completed\n'), + onFinish: () => console.log('\n✅ Data analysis example completed\n'), onError: (error) => console.error('❌ Error:', error), }, provider ); } + + // Example 6: File Creation and Manipulation + console.log('=== Example 6: File Creation and Manipulation ===\n'); + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: `You are a helpful assistant with filesystem access. Available directories are /shared and /tmp. You can create, read, write, and manage files in these directories.`, + }, + { + role: MessageRole.user, + content: + 'Can you create a simple todo list file at /tmp/todo.txt with 3 sample tasks, then read it back to me?', + }, + ], + max_tokens: 300, + }, + { + onOpen: () => console.log('🚀 Starting file manipulation example...'), + onContent: (content) => process.stdout.write(content), + onTool: (toolCall) => { + console.log(`\n🔧 Tool called: ${toolCall.function.name}`); + console.log(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onFinish: () => + console.log('\n✅ File manipulation example completed\n'), + onError: (error) => console.error('❌ Error:', error), + }, + provider + ); } catch (error) { if ( error instanceof Error && diff --git a/examples/mcp/mcp-servers/README.md b/examples/mcp/mcp-servers/README.md new file mode 100644 index 0000000..20e4941 --- /dev/null +++ b/examples/mcp/mcp-servers/README.md @@ -0,0 +1,241 @@ +# MCP Servers + +This directory contains Model Context Protocol (MCP) servers that demonstrate how to build and integrate custom tools with the Inference Gateway. + +## Available Servers + +### 🌐 Web Search Server (`web-search/`) +- **Port**: 3001 +- **Tools**: `fetch_url`, `search_web`, `get_page_title` +- **Purpose**: Provides web content fetching and search capabilities +- **Features**: HTTP requests, simulated search, HTML parsing + +### 📁 Filesystem Server (`filesystem/`) +- **Port**: 3000 +- **Tools**: `read_file`, `write_file`, `list_directory`, `create_directory`, `delete_file`, `file_info` +- **Purpose**: Safe filesystem operations within allowed directories +- **Features**: File I/O, directory management, security restrictions + +## Quick Start + +### Individual Server Development + +Each server can be run independently for development: + +```bash +# Web Search Server +cd web-search +npm install +npm run dev + +# Filesystem Server +cd filesystem +npm install +npm run dev +``` + +### Docker Compose Integration + +All servers are configured to work together with the Inference Gateway: + +```bash +# From the main MCP example directory +docker-compose up -d +``` + +This will start: +- Inference Gateway (port 8080) +- MCP Filesystem Server (port 3000) +- MCP Web Search Server (port 3001) +- Optional: Ollama (port 11434) + +## Server Architecture + +Each MCP server follows a consistent structure: + +``` +server-name/ +├── package.json # Dependencies and scripts +├── index.js # Main server implementation +└── README.md # Server-specific documentation +``` + +### Core Endpoints + +All MCP servers implement these standard endpoints: + +- `GET /mcp` - Server information and capabilities +- `POST /mcp/tools/list` - List available tools +- `POST /mcp/tools/call` - Execute tools +- `GET /health` - Health check + +## Tool Development + +### Adding New Tools + +To add a new tool to an existing server: + +1. **Define the tool** in the `/mcp/tools/list` endpoint: +```javascript +{ + name: 'my_new_tool', + description: 'Description of what the tool does', + inputSchema: { + type: 'object', + properties: { + param1: { + type: 'string', + description: 'Parameter description' + } + }, + required: ['param1'] + } +} +``` + +2. **Implement the handler** function: +```javascript +async function handleMyNewTool(args, res) { + const { param1 } = args; + + try { + // Tool logic here + const result = await doSomething(param1); + + res.json({ + content: [ + { + type: 'text', + text: `Result: ${result}` + } + ] + }); + } catch (error) { + res.json({ + content: [ + { + type: 'text', + text: `Error: ${error.message}` + } + ] + }); + } +} +``` + +3. **Add the case** to the tool execution switch: +```javascript +case 'my_new_tool': + await handleMyNewTool(args, res); + break; +``` + +### Creating New Servers + +To create a new MCP server: + +1. **Create directory structure**: +```bash +mkdir mcp-servers/my-server +cd mcp-servers/my-server +``` + +2. **Create package.json**: +```json +{ + "name": "mcp-my-server", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5" + } +} +``` + +3. **Implement the server** following the existing patterns +4. **Add to docker-compose.yml** for integration +5. **Update Inference Gateway** MCP_SERVERS configuration + +## Integration with Inference Gateway + +The servers are configured to work with the Inference Gateway through environment variables: + +```yaml +# In docker-compose.yml +environment: + MCP_ENABLE: 'true' + MCP_EXPOSE: 'true' + MCP_SERVERS: 'filesystem=http://mcp-filesystem:3000/mcp,web-search=http://mcp-web-search:3001/mcp' +``` + +## Security Considerations + +### Filesystem Server +- Restricts operations to allowed directories +- Validates all file paths +- Prevents directory traversal attacks +- Implements proper error handling + +### Web Search Server +- Includes request timeouts +- Limits response sizes +- Handles various content types safely +- Provides safe error messages + +### General Security +- All servers run with minimal privileges +- Docker containers are isolated +- Health checks monitor server status +- Graceful shutdown handling + +## Testing + +Each server includes health check endpoints and can be tested independently: + +```bash +# Test server health +curl http://localhost:3000/health +curl http://localhost:3001/health + +# Test MCP endpoints +curl http://localhost:3000/mcp +curl -X POST http://localhost:3000/mcp/tools/list +``` + +## Monitoring + +Monitor server logs during development: + +```bash +# Follow logs for all services +docker-compose logs -f + +# Follow logs for specific service +docker-compose logs -f mcp-filesystem +docker-compose logs -f mcp-web-search +``` + +## Examples + +See the main MCP example (`../index.ts`) for complete usage examples showing how to: +- Discover available MCP tools +- Execute filesystem operations +- Perform web searches and content fetching +- Handle tool responses and errors + +## Contributing + +When contributing new servers or tools: + +1. Follow the established patterns and conventions +2. Include comprehensive error handling +3. Add proper validation for all inputs +4. Document all tools and parameters +5. Include health check endpoints +6. Test thoroughly with the Inference Gateway +7. Update this README with new server information diff --git a/examples/mcp/mcp-servers/filesystem/README.md b/examples/mcp/mcp-servers/filesystem/README.md new file mode 100644 index 0000000..2eb6333 --- /dev/null +++ b/examples/mcp/mcp-servers/filesystem/README.md @@ -0,0 +1,200 @@ +# MCP Filesystem Server + +A Model Context Protocol (MCP) server that provides filesystem operations for file and directory management. + +## Features + +- **read_file**: Read the contents of a file +- **write_file**: Write content to a file (creates directories as needed) +- **list_directory**: List the contents of a directory with file information +- **create_directory**: Create new directories (recursive) +- **delete_file**: Delete files safely +- **file_info**: Get detailed information about files and directories + +## Security + +This server implements directory restrictions to ensure safe file operations: + +- Only operates within allowed directories (configurable via `ALLOWED_DIRECTORIES`) +- Validates all paths to prevent directory traversal attacks +- Provides clear error messages for permission issues + +## Installation + +```bash +npm install +``` + +## Usage + +### Development + +```bash +npm run dev +``` + +### Production + +```bash +npm start +``` + +The server will start on port 3000 by default. You can change this by setting the `PORT` environment variable. + +## Configuration + +### Environment Variables + +- `PORT`: Server port (default: 3000) +- `HOST`: Server host (default: 0.0.0.0) +- `ALLOWED_DIRECTORIES`: Comma-separated list of allowed directories (default: "/shared,/tmp") +- `NODE_ENV`: Environment (development/production) + +### Example Configuration + +```bash +export ALLOWED_DIRECTORIES="/shared,/tmp,/workspace/data" +export PORT=3000 +npm start +``` + +## API Endpoints + +### Server Information + +``` +GET /mcp +``` + +Returns server capabilities and metadata. + +### List Tools + +``` +POST /mcp/tools/list +``` + +Returns all available MCP tools. + +### Execute Tools + +``` +POST /mcp/tools/call +``` + +Execute a specific tool with provided arguments. + +### Health Check + +``` +GET /health +``` + +Returns server health status and configuration. + +## Example Tool Usage + +### Read File + +```json +{ + "name": "read_file", + "arguments": { + "path": "/shared/example.txt" + } +} +``` + +### Write File + +```json +{ + "name": "write_file", + "arguments": { + "path": "/shared/new-file.txt", + "content": "Hello, MCP World!" + } +} +``` + +### List Directory + +```json +{ + "name": "list_directory", + "arguments": { + "path": "/shared" + } +} +``` + +### Create Directory + +```json +{ + "name": "create_directory", + "arguments": { + "path": "/shared/new-folder" + } +} +``` + +### Delete File + +```json +{ + "name": "delete_file", + "arguments": { + "path": "/shared/unwanted-file.txt" + } +} +``` + +### Get File Info + +```json +{ + "name": "file_info", + "arguments": { + "path": "/shared/example.txt" + } +} +``` + +## Integration with Inference Gateway + +This server is designed to work with the Inference Gateway's MCP support. Add it to your gateway configuration: + +```yaml +MCP_SERVERS: 'filesystem=http://mcp-filesystem:3000/mcp' +``` + +## Sample Files + +The server automatically creates sample files in allowed directories on startup to help with testing and demonstration. + +## Error Handling + +The server provides detailed error messages for common scenarios: + +- File not found +- Permission denied +- Directory traversal attempts +- Invalid arguments +- Path outside allowed directories + +## Extending the Server + +To add new filesystem tools: + +1. Add the tool definition to the `/mcp/tools/list` endpoint +2. Create a handler function for the tool +3. Add the case to the switch statement in `/mcp/tools/call` +4. Ensure proper path validation and error handling + +## Security Considerations + +- This server implements basic security measures but should be reviewed for production use +- Consider additional authentication and authorization mechanisms +- Monitor file system usage and implement quotas if needed +- Regularly audit allowed directories and permissions diff --git a/examples/mcp/mcp-servers/filesystem/index.js b/examples/mcp/mcp-servers/filesystem/index.js new file mode 100644 index 0000000..f1fda76 --- /dev/null +++ b/examples/mcp/mcp-servers/filesystem/index.js @@ -0,0 +1,621 @@ +/** + * MCP Filesystem Server + * + * This is a Model Context Protocol (MCP) server that provides filesystem + * operations. It uses the official MCP TypeScript SDK and implements + * the proper MCP protocol with Streamable HTTP transport. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import express from 'express'; +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; +import { promises as fs } from 'node:fs'; +import path from 'node:path'; + +// Express app for HTTP transport +const app = express(); +app.use(express.json()); + +// Map to store transports by session ID +const transports = {}; + +// Allowed directories (configurable via environment) +const allowedDirectories = ( + process.env.ALLOWED_DIRECTORIES || '/shared,/tmp' +).split(','); + +console.log('Allowed directories:', allowedDirectories); + +/** + * Check if a path is within allowed directories + */ +function isPathAllowed(filePath) { + const normalizedPath = path.resolve(filePath); + return allowedDirectories.some((allowedDir) => { + const normalizedAllowed = path.resolve(allowedDir); + return normalizedPath.startsWith(normalizedAllowed); + }); +} + +/** + * Create and configure the MCP server + */ +function createMcpServer() { + const mcpServer = new McpServer({ + name: 'filesystem', + version: '1.0.0', + }); + + // Tool: Read file content + mcpServer.tool( + 'read_file', + { + path: z.string().describe('The file path to read'), + }, + async ({ path: filePath }) => { + if (!isPathAllowed(filePath)) { + throw new Error( + `Access denied: ${filePath} is not in allowed directories` + ); + } + + try { + console.log(`Reading file: ${filePath}`); + const content = await fs.readFile(filePath, 'utf8'); + + return { + content: [ + { + type: 'text', + text: `File: ${filePath}\nSize: ${content.length} characters\n\nContent:\n${content}`, + }, + ], + }; + } catch (error) { + console.error(`Failed to read file ${filePath}:`, error.message); + + let errorMessage = `Failed to read file: ${filePath}\n`; + if (error.code === 'ENOENT') { + errorMessage += 'File does not exist'; + } else if (error.code === 'EACCES') { + errorMessage += 'Permission denied'; + } else if (error.code === 'EISDIR') { + errorMessage += 'Path is a directory, not a file'; + } else { + errorMessage += error.message; + } + + return { + content: [ + { + type: 'text', + text: errorMessage, + }, + ], + }; + } + } + ); + + // Tool: Write file content + mcpServer.tool( + 'write_file', + { + path: z.string().describe('The file path to write to'), + content: z.string().describe('The content to write to the file'), + }, + async ({ path: filePath, content }) => { + if (!isPathAllowed(filePath)) { + throw new Error( + `Access denied: ${filePath} is not in allowed directories` + ); + } + + try { + console.log(`Writing to file: ${filePath}`); + + // Ensure directory exists + const dir = path.dirname(filePath); + await fs.mkdir(dir, { recursive: true }); + + await fs.writeFile(filePath, content, 'utf8'); + + return { + content: [ + { + type: 'text', + text: `Successfully wrote ${content.length} characters to: ${filePath}`, + }, + ], + }; + } catch (error) { + console.error(`Failed to write file ${filePath}:`, error.message); + + return { + content: [ + { + type: 'text', + text: `Failed to write file: ${filePath}\nError: ${error.message}`, + }, + ], + }; + } + } + ); + + // Tool: List directory contents + mcpServer.tool( + 'list_directory', + { + path: z.string().describe('The directory path to list'), + }, + async ({ path: dirPath }) => { + if (!isPathAllowed(dirPath)) { + throw new Error( + `Access denied: ${dirPath} is not in allowed directories` + ); + } + + try { + console.log(`Listing directory: ${dirPath}`); + + const entries = await fs.readdir(dirPath, { withFileTypes: true }); + + const items = await Promise.all( + entries.map(async (entry) => { + const fullPath = path.join(dirPath, entry.name); + try { + const stats = await fs.stat(fullPath); + return { + name: entry.name, + type: entry.isDirectory() ? 'directory' : 'file', + size: stats.size, + modified: stats.mtime.toISOString(), + }; + } catch (error) { + console.warn( + `Could not get stats for ${entry.name}:`, + error.message + ); + return { + name: entry.name, + type: entry.isDirectory() ? 'directory' : 'file', + size: 'unknown', + modified: 'unknown', + }; + } + }) + ); + + const result = + `Directory: ${dirPath}\nTotal items: ${items.length}\n\n` + + items + .map( + (item) => + `${item.type === 'directory' ? '📁' : '📄'} ${item.name} (${item.size} bytes, modified: ${item.modified})` + ) + .join('\n'); + + return { + content: [ + { + type: 'text', + text: result, + }, + ], + }; + } catch (error) { + console.error(`Failed to list directory ${dirPath}:`, error.message); + + let errorMessage = `Failed to list directory: ${dirPath}\n`; + if (error.code === 'ENOENT') { + errorMessage += 'Directory does not exist'; + } else if (error.code === 'EACCES') { + errorMessage += 'Permission denied'; + } else if (error.code === 'ENOTDIR') { + errorMessage += 'Path is not a directory'; + } else { + errorMessage += error.message; + } + + return { + content: [ + { + type: 'text', + text: errorMessage, + }, + ], + }; + } + } + ); + + // Tool: Create directory + mcpServer.tool( + 'create_directory', + { + path: z.string().describe('The directory path to create'), + }, + async ({ path: dirPath }) => { + if (!isPathAllowed(dirPath)) { + throw new Error( + `Access denied: ${dirPath} is not in allowed directories` + ); + } + + try { + console.log(`Creating directory: ${dirPath}`); + + await fs.mkdir(dirPath, { recursive: true }); + + return { + content: [ + { + type: 'text', + text: `Successfully created directory: ${dirPath}`, + }, + ], + }; + } catch (error) { + console.error(`Failed to create directory ${dirPath}:`, error.message); + + return { + content: [ + { + type: 'text', + text: `Failed to create directory: ${dirPath}\nError: ${error.message}`, + }, + ], + }; + } + } + ); + + // Tool: Delete file + mcpServer.tool( + 'delete_file', + { + path: z.string().describe('The file path to delete'), + }, + async ({ path: filePath }) => { + if (!isPathAllowed(filePath)) { + throw new Error( + `Access denied: ${filePath} is not in allowed directories` + ); + } + + try { + console.log(`Deleting file: ${filePath}`); + + await fs.unlink(filePath); + + return { + content: [ + { + type: 'text', + text: `Successfully deleted file: ${filePath}`, + }, + ], + }; + } catch (error) { + console.error(`Failed to delete file ${filePath}:`, error.message); + + let errorMessage = `Failed to delete file: ${filePath}\n`; + if (error.code === 'ENOENT') { + errorMessage += 'File does not exist'; + } else if (error.code === 'EACCES') { + errorMessage += 'Permission denied'; + } else if (error.code === 'EISDIR') { + errorMessage += + 'Path is a directory, use remove directory tool instead'; + } else { + errorMessage += error.message; + } + + return { + content: [ + { + type: 'text', + text: errorMessage, + }, + ], + }; + } + } + ); + + // Tool: Get file info + mcpServer.tool( + 'file_info', + { + path: z.string().describe('The file or directory path to get info for'), + }, + async ({ path: filePath }) => { + if (!isPathAllowed(filePath)) { + throw new Error( + `Access denied: ${filePath} is not in allowed directories` + ); + } + + try { + console.log(`Getting info for: ${filePath}`); + + const stats = await fs.stat(filePath); + + const info = { + path: filePath, + type: stats.isDirectory() ? 'directory' : 'file', + size: stats.size, + created: stats.birthtime.toISOString(), + modified: stats.mtime.toISOString(), + accessed: stats.atime.toISOString(), + permissions: stats.mode.toString(8), + isReadable: !!(stats.mode & parseInt('444', 8)), + isWritable: !!(stats.mode & parseInt('222', 8)), + isExecutable: !!(stats.mode & parseInt('111', 8)), + }; + + const result = + `File Information:\n\n` + + `Path: ${info.path}\n` + + `Type: ${info.type}\n` + + `Size: ${info.size} bytes\n` + + `Created: ${info.created}\n` + + `Modified: ${info.modified}\n` + + `Accessed: ${info.accessed}\n` + + `Permissions: ${info.permissions}\n` + + `Readable: ${info.isReadable}\n` + + `Writable: ${info.isWritable}\n` + + `Executable: ${info.isExecutable}`; + + return { + content: [ + { + type: 'text', + text: result, + }, + ], + }; + } catch (error) { + console.error(`Failed to get info for ${filePath}:`, error.message); + + let errorMessage = `Failed to get file info: ${filePath}\n`; + if (error.code === 'ENOENT') { + errorMessage += 'File or directory does not exist'; + } else if (error.code === 'EACCES') { + errorMessage += 'Permission denied'; + } else { + errorMessage += error.message; + } + + return { + content: [ + { + type: 'text', + text: errorMessage, + }, + ], + }; + } + } + ); + + return mcpServer; +} + +/** + * Setup MCP endpoints for proper Model Context Protocol communication + */ +function setupSessionRoutes() { + // Handle POST requests for MCP communication + app.post('/mcp', async (req, res) => { + try { + console.log('MCP POST request received:'); + console.log(' Headers:', JSON.stringify(req.headers, null, 2)); + console.log(' Body:', JSON.stringify(req.body, null, 2)); + + // Fix missing Accept headers for compatibility with Go MCP clients + // The StreamableHTTPServerTransport requires both application/json and text/event-stream + const accept = req.headers.accept || req.headers.Accept; + if ( + !accept || + !accept.includes('application/json') || + !accept.includes('text/event-stream') + ) { + console.log('Adding missing Accept headers for MCP compatibility'); + req.headers.accept = 'application/json, text/event-stream'; + } + + // Check for existing session ID + const sessionId = req.headers['mcp-session-id']; + let transport; + + if (sessionId && transports[sessionId]) { + // Reuse existing transport + transport = transports[sessionId]; + } else { + // Create new transport for new session + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (newSessionId) => { + console.log(`MCP session initialized: ${newSessionId}`); + // Store the transport by session ID + transports[newSessionId] = transport; + }, + }); + + // Clean up transport when closed + transport.onclose = () => { + if (transport.sessionId) { + console.log(`MCP session closed: ${transport.sessionId}`); + delete transports[transport.sessionId]; + } + }; + + // Create and connect MCP server + const server = createMcpServer(); + await server.connect(transport); + } + + // Handle the MCP request + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } + }); + + // Handle GET requests for SSE (server-to-client notifications) + app.get('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + const transport = transports[sessionId]; + await transport.handleRequest(req, res); + }); + + // Handle DELETE requests for session termination + app.delete('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + const transport = transports[sessionId]; + await transport.handleRequest(req, res); + }); +} + +/** + * Initialize sample files + */ +async function initializeSampleFiles() { + try { + // Create sample files in allowed directories + for (const dir of allowedDirectories) { + try { + await fs.access(dir); + + const sampleFile = path.join(dir, 'mcp-filesystem-example.txt'); + const sampleContent = `Hello from MCP Filesystem Server! + +This is a sample file created by the MCP Filesystem Server. +Created at: ${new Date().toISOString()} + +You can use the following MCP tools to interact with this file: +- read_file: Read this file's content +- write_file: Modify this file +- file_info: Get detailed information about this file +- list_directory: List all files in this directory +- delete_file: Delete this file + +Available directories: ${allowedDirectories.join(', ')} +`; + + await fs.writeFile(sampleFile, sampleContent); + console.log(`Created sample file: ${sampleFile}`); + } catch (error) { + console.log(`Could not create sample file in ${dir}:`, error.message); + } + } + } catch (error) { + console.error('Error initializing sample files:', error); + } +} + +/** + * Health check endpoint + */ +app.get('/health', (req, res) => { + const healthStatus = { + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + allowedDirectories, + service: 'mcp-filesystem', + version: '1.0.0', + activeSessions: Object.keys(transports).length, + }; + + console.log('Health check requested:', healthStatus); + res.json(healthStatus); +}); + +/** + * Start the server + */ +async function startServer() { + const port = process.env.PORT || 3000; + const host = process.env.HOST || '0.0.0.0'; + + // Set up session routes + setupSessionRoutes(); + + app.listen(port, host, async () => { + console.log(`MCP Filesystem server running on http://${host}:${port}`); + console.log('Protocol: Model Context Protocol (MCP)'); + console.log('Transport: Streamable HTTP'); + console.log('Available endpoints:'); + console.log(' POST /mcp - MCP protocol endpoint'); + console.log( + ' GET /mcp - SSE notifications (with session-id header)' + ); + console.log( + ' DELETE /mcp - Session termination (with session-id header)' + ); + console.log(' GET /health - Health check'); + console.log('Available tools:'); + console.log(' - read_file - Read content from a file'); + console.log(' - write_file - Write content to a file'); + console.log(' - list_directory - List directory contents'); + console.log(' - create_directory - Create a new directory'); + console.log(' - delete_file - Delete a file'); + console.log(' - move_file - Move/rename a file'); + console.log('Allowed directories:', allowedDirectories); + + // Initialize sample files + await initializeSampleFiles(); + + console.log('MCP Filesystem server ready for connections'); + }); +} + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('Received SIGTERM, shutting down gracefully'); + // Close all transports + Object.values(transports).forEach((transport) => { + if (transport.close) transport.close(); + }); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.log('Received SIGINT, shutting down gracefully'); + // Close all transports + Object.values(transports).forEach((transport) => { + if (transport.close) transport.close(); + }); + process.exit(0); +}); + +// Start the server +startServer().catch((error) => { + console.error('Failed to start server:', error); + process.exit(1); +}); diff --git a/examples/mcp/mcp-servers/filesystem/package-lock.json b/examples/mcp/mcp-servers/filesystem/package-lock.json new file mode 100644 index 0000000..eb070a4 --- /dev/null +++ b/examples/mcp/mcp-servers/filesystem/package-lock.json @@ -0,0 +1,1433 @@ +{ + "name": "mcp-filesystem-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-filesystem-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "cors": "^2.8.5", + "express": "^4.18.2", + "zod": "^3.22.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz", + "integrity": "sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz", + "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.30", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.30.tgz", + "integrity": "sha512-VolhdEtu6TJr/fzGuHA/SZ5ixvXqA6ADOG9VRcQ3rdOKmF5hkmcJbyaQjUH5BgmpA9gej++zYRX7zjSmdReIwA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/examples/mcp/mcp-servers/filesystem/package.json b/examples/mcp/mcp-servers/filesystem/package.json new file mode 100644 index 0000000..93e0406 --- /dev/null +++ b/examples/mcp/mcp-servers/filesystem/package.json @@ -0,0 +1,22 @@ +{ + "name": "mcp-filesystem-server", + "version": "1.0.0", + "description": "MCP Filesystem Server for file operations", + "type": "module", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "cors": "^2.8.5", + "express": "^4.18.2", + "zod": "^3.22.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "author": "Inference Gateway Team", + "license": "MIT" +} diff --git a/examples/mcp/mcp-servers/web-search/README.md b/examples/mcp/mcp-servers/web-search/README.md new file mode 100644 index 0000000..35cc9b9 --- /dev/null +++ b/examples/mcp/mcp-servers/web-search/README.md @@ -0,0 +1,130 @@ +# MCP Web Search Server + +A Model Context Protocol (MCP) server that provides web search and URL fetching capabilities. + +## Features + +- **fetch_url**: Fetch content from any URL with error handling and timeout support +- **search_web**: Simulated web search functionality (can be replaced with real search APIs) +- **get_page_title**: Extract page titles from web pages using HTML parsing + +## Installation + +```bash +npm install +``` + +## Usage + +### Development + +```bash +npm run dev +``` + +### Production + +```bash +npm start +``` + +The server will start on port 3001 by default. You can change this by setting the `PORT` environment variable. + +## API Endpoints + +### Server Information + +``` +GET /mcp +``` + +Returns server capabilities and metadata. + +### List Tools + +``` +POST /mcp/tools/list +``` + +Returns all available MCP tools. + +### Execute Tools + +``` +POST /mcp/tools/call +``` + +Execute a specific tool with provided arguments. + +### Health Check + +``` +GET /health +``` + +Returns server health status. + +## Example Tool Usage + +### Fetch URL + +```json +{ + "name": "fetch_url", + "arguments": { + "url": "https://api.github.com/users/octocat", + "timeout": 5000 + } +} +``` + +### Search Web + +```json +{ + "name": "search_web", + "arguments": { + "query": "machine learning tutorials", + "limit": 5 + } +} +``` + +### Get Page Title + +```json +{ + "name": "get_page_title", + "arguments": { + "url": "https://github.com" + } +} +``` + +## Environment Variables + +- `PORT`: Server port (default: 3001) +- `HOST`: Server host (default: 0.0.0.0) +- `NODE_ENV`: Environment (development/production) + +## Integration with Inference Gateway + +This server is designed to work with the Inference Gateway's MCP support. Add it to your gateway configuration: + +```yaml +MCP_SERVERS: 'web-search=http://mcp-web-search:3001/mcp' +``` + +## Extending the Server + +To add new tools: + +1. Add the tool definition to the `/mcp/tools/list` endpoint +2. Add a handler function for the tool +3. Add the case to the switch statement in `/mcp/tools/call` + +## Security Considerations + +- This is a demonstration server and should not be used in production without proper security measures +- Add rate limiting, authentication, and input validation for production use +- Consider using environment variables for sensitive configuration diff --git a/examples/mcp/mcp-servers/web-search/index-http.js b/examples/mcp/mcp-servers/web-search/index-http.js new file mode 100644 index 0000000..6ed6d20 --- /dev/null +++ b/examples/mcp/mcp-servers/web-search/index-http.js @@ -0,0 +1,331 @@ +/** + * HTTP-based Web Search Server using MCP SDK + * + * This server uses the official MCP TypeScript SDK with StreamableHTTPServerTransport + * in stateless mode, which responds with plain HTTP JSON-RPC instead of SSE. + * This is compatible with Go MCP clients that expect standard HTTP responses. + */ + +import express from 'express'; +import axios from 'axios'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { z } from 'zod'; + +const app = express(); +app.use(express.json()); + +/** + * Generate simulated search results + */ +function generateSearchResults(query, limit) { + const results = []; + const domains = [ + 'example.com', + 'wikipedia.org', + 'github.com', + 'stackoverflow.com', + 'medium.com', + ]; + + for (let i = 0; i < Math.min(limit, 10); i++) { + const domain = domains[i % domains.length]; + const title = `${query} - Result ${i + 1}`; + const url = `https://${domain}/${query + .toLowerCase() + .replace(/\s+/g, '-')}-${i + 1}`; + const description = `This is a simulated search result for "${query}". It would normally contain relevant information about your search query.`; + + results.push(`${i + 1}. ${title}\n ${url}\n ${description}\n`); + } + + return results.join('\n'); +} + +/** + * Create and configure the MCP server + */ +function createServer() { + const server = new McpServer( + { + name: 'web-search-server', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + }, + } + ); + + // Add fetch_url tool + server.tool( + 'fetch_url', + { + description: 'Fetch content from a URL', + inputSchema: { + type: 'object', + properties: { + url: z.string().url(), + timeout: z.number().min(1000).max(30000).optional().default(10000), + }, + required: ['url'], + }, + }, + async ({ url, timeout = 10000 }) => { + try { + console.log(`Fetching URL: ${url}`); + const response = await axios.get(url, { + timeout, + headers: { + 'User-Agent': 'HTTP-Web-Search-Server/1.0.0', + }, + maxRedirects: 5, + validateStatus: (status) => status < 500, + }); + + const contentType = response.headers['content-type'] || ''; + let content; + if (contentType.includes('application/json')) { + content = JSON.stringify(response.data, null, 2); + } else if (contentType.includes('text/')) { + content = response.data.toString(); + } else { + content = `Binary content (${contentType}), size: ${ + JSON.stringify(response.data).length + } bytes`; + } + + // Truncate very large responses + if (content.length > 10000) { + content = content.substring(0, 10000) + '\n\n... (content truncated)'; + } + + return { + content: [ + { + type: 'text', + text: `URL: ${url}\nStatus: ${response.status} ${response.statusText}\nContent-Type: ${contentType}\n\nContent:\n${content}`, + }, + ], + }; + } catch (error) { + console.error(`Failed to fetch URL ${url}:`, error.message); + let errorMessage = `Failed to fetch URL: ${url}\n`; + if (error.code === 'ENOTFOUND') { + errorMessage += 'Domain not found'; + } else if (error.code === 'ECONNREFUSED') { + errorMessage += 'Connection refused'; + } else if (error.code === 'ETIMEDOUT') { + errorMessage += 'Request timed out'; + } else if (error.response) { + errorMessage += `HTTP ${error.response.status}: ${error.response.statusText}`; + } else { + errorMessage += error.message; + } + + return { + content: [ + { + type: 'text', + text: errorMessage, + }, + ], + }; + } + } + ); + + // Add search_web tool + server.tool( + 'search_web', + { + description: 'Perform a web search', + inputSchema: { + type: 'object', + properties: { + query: z.string().min(1).max(500), + limit: z.number().min(1).max(20).optional().default(5), + }, + required: ['query'], + }, + }, + async ({ query, limit = 5 }) => { + console.log(`Searching for: "${query}" (limit: ${limit})`); + const searchResults = generateSearchResults(query, limit); + + return { + content: [ + { + type: 'text', + text: `Search Results for "${query}":\n\n${searchResults}`, + }, + ], + }; + } + ); + + // Add get_page_title tool + server.tool( + 'get_page_title', + { + description: 'Extract title from a web page', + inputSchema: { + type: 'object', + properties: { + url: z.string().url(), + }, + required: ['url'], + }, + }, + async ({ url }) => { + try { + console.log(`Extracting title from: ${url}`); + const response = await axios.get(url, { + timeout: 10000, + headers: { + 'User-Agent': 'HTTP-Web-Search-Server/1.0.0', + }, + }); + + const titleMatch = response.data.match(/]*>([^<]+)<\/title>/i); + const title = titleMatch ? titleMatch[1].trim() : 'No title found'; + + return { + content: [ + { + type: 'text', + text: `Title: ${title}\nURL: ${url}`, + }, + ], + }; + } catch (error) { + console.error(`Failed to extract title from ${url}:`, error.message); + + return { + content: [ + { + type: 'text', + text: `Failed to extract title from: ${url}\nError: ${error.message}`, + }, + ], + }; + } + } + ); + + return server; +} + +/** + * Handle MCP requests using stateless mode + */ +app.post('/mcp', async (req, res) => { + try { + console.log('HTTP JSON-RPC request received:'); + console.log(' Body:', JSON.stringify(req.body, null, 2)); + + // Create new server and transport instances for each request (stateless mode) + const server = createServer(); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, // Stateless mode + }); + + // Clean up on request close + res.on('close', () => { + console.log('Request closed'); + transport.close(); + server.close(); + }); + + // Connect server to transport + await server.connect(transport); + + // Handle the request + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + data: error.message, + }, + id: req.body?.id || null, + }); + } + } +}); + +// Handle unsupported methods for stateless mode +app.get('/mcp', async (req, res) => { + console.log('Received GET MCP request'); + res.status(405).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed in stateless mode.', + }, + id: null, + }); +}); + +app.delete('/mcp', async (req, res) => { + console.log('Received DELETE MCP request'); + res.status(405).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed in stateless mode.', + }, + id: null, + }); +}); + +// Health check endpoint +app.get('/health', (req, res) => { + const healthStatus = { + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + service: 'http-web-search', + version: '1.0.0', + protocol: 'HTTP JSON-RPC', + }; + + console.log('Health check requested:', healthStatus); + res.json(healthStatus); +}); + +// Start the server +const port = process.env.PORT || 3001; +const host = process.env.HOST || '0.0.0.0'; + +app.listen(port, host, () => { + console.log(`HTTP Web Search server running on http://${host}:${port}`); + console.log('Protocol: HTTP JSON-RPC 2.0'); + console.log('Available endpoints:'); + console.log(' POST /mcp - JSON-RPC endpoint'); + console.log(' GET /health - Health check'); + console.log('Available methods:'); + console.log(' - initialize - Initialize the server'); + console.log(' - tools/list - List available tools'); + console.log(' - tools/call - Call a tool'); + console.log('Available tools:'); + console.log(' - fetch_url - Fetch content from a URL'); + console.log(' - search_web - Perform web search (simulated)'); + console.log(' - get_page_title - Extract title from a web page'); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('Received SIGTERM, shutting down gracefully'); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.log('Received SIGINT, shutting down gracefully'); + process.exit(0); +}); diff --git a/examples/mcp/mcp-servers/web-search/index.js b/examples/mcp/mcp-servers/web-search/index.js new file mode 100644 index 0000000..592045e --- /dev/null +++ b/examples/mcp/mcp-servers/web-search/index.js @@ -0,0 +1,363 @@ +/** + * MCP Web Search Server + * + * This is a Model Context Protocol (MCP) server that provides web search + * and URL fetching capabilities. It uses the official MCP TypeScript SDK + * and implements the proper MCP protocol with Streamable HTTP transport. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import express from 'express'; +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; +import axios from 'axios'; + +// Express app for HTTP transport +const app = express(); +app.use(express.json()); + +// Map to store transports by session ID +const transports = {}; + +/** + * Create and configure the MCP server + */ +function createMcpServer() { + const mcpServer = new McpServer({ + name: 'web-search', + version: '1.0.0', + }); + + // Tool: Fetch URL content + mcpServer.tool( + 'fetch_url', + { + url: z.string().url().describe('The URL to fetch content from'), + timeout: z + .number() + .min(1000) + .max(30000) + .default(10000) + .describe('Request timeout in milliseconds'), + }, + async ({ url, timeout = 10000 }) => { + try { + console.log(`Fetching URL: ${url}`); + + const response = await axios.get(url, { + timeout, + headers: { + 'User-Agent': 'MCP-Web-Search-Server/1.0.0', + }, + maxRedirects: 5, + validateStatus: (status) => status < 500, // Accept 4xx but not 5xx + }); + + const contentType = response.headers['content-type'] || ''; + let content; + + if (contentType.includes('application/json')) { + content = JSON.stringify(response.data, null, 2); + } else if (contentType.includes('text/')) { + content = response.data.toString(); + } else { + content = `Binary content (${contentType}), size: ${ + JSON.stringify(response.data).length + } bytes`; + } + + // Truncate very large responses + if (content.length > 10000) { + content = content.substring(0, 10000) + '\n\n... (content truncated)'; + } + + return { + content: [ + { + type: 'text', + text: `URL: ${url}\nStatus: ${response.status} ${response.statusText}\nContent-Type: ${contentType}\n\nContent:\n${content}`, + }, + ], + }; + } catch (error) { + console.error(`Failed to fetch URL ${url}:`, error.message); + + let errorMessage = `Failed to fetch URL: ${url}\n`; + + if (error.code === 'ENOTFOUND') { + errorMessage += 'Domain not found'; + } else if (error.code === 'ECONNREFUSED') { + errorMessage += 'Connection refused'; + } else if (error.code === 'ETIMEDOUT') { + errorMessage += 'Request timed out'; + } else if (error.response) { + errorMessage += `HTTP ${error.response.status}: ${error.response.statusText}`; + } else { + errorMessage += error.message; + } + + return { + content: [ + { + type: 'text', + text: errorMessage, + }, + ], + }; + } + } + ); + + // Tool: Web search (simulated) + mcpServer.tool( + 'search_web', + { + query: z.string().min(1).max(500).describe('The search query to execute'), + limit: z + .number() + .min(1) + .max(20) + .default(5) + .describe('Maximum number of results to return'), + }, + async ({ query, limit = 5 }) => { + console.log(`Searching for: "${query}" (limit: ${limit})`); + + // Generate simulated search results + const searchResults = generateSearchResults(query, limit); + + return { + content: [ + { + type: 'text', + text: `Search Results for "${query}":\n\n${searchResults}`, + }, + ], + }; + } + ); + + // Tool: Get page title + mcpServer.tool( + 'get_page_title', + { + url: z.string().url().describe('The URL to extract the title from'), + }, + async ({ url }) => { + try { + console.log(`Extracting title from: ${url}`); + + const response = await axios.get(url, { + timeout: 10000, + headers: { + 'User-Agent': 'MCP-Web-Search-Server/1.0.0', + }, + }); + + // Simple title extraction using regex (cheerio would require additional dependency) + const titleMatch = response.data.match(/]*>([^<]+)<\/title>/i); + const title = titleMatch ? titleMatch[1].trim() : 'No title found'; + + return { + content: [ + { + type: 'text', + text: `Title: ${title}\nURL: ${url}`, + }, + ], + }; + } catch (error) { + console.error(`Failed to extract title from ${url}:`, error.message); + + return { + content: [ + { + type: 'text', + text: `Failed to extract title from: ${url}\nError: ${error.message}`, + }, + ], + }; + } + } + ); + + return mcpServer; +} + +/** + * Generate simulated search results + */ +function generateSearchResults(query, limit) { + const results = []; + const domains = [ + 'example.com', + 'wikipedia.org', + 'github.com', + 'stackoverflow.com', + 'medium.com', + ]; + + for (let i = 0; i < Math.min(limit, 10); i++) { + const domain = domains[i % domains.length]; + const title = `${query} - Result ${i + 1}`; + const url = `https://${domain}/${query + .toLowerCase() + .replace(/\s+/g, '-')}-${i + 1}`; + const description = `This is a simulated search result for "${query}". It would normally contain relevant information about your search query.`; + + results.push(`${i + 1}. ${title}\n ${url}\n ${description}\n`); + } + + return results.join('\n'); +} + +// Handle POST requests for MCP communication +app.post('/mcp', async (req, res) => { + try { + console.log('MCP POST request received:'); + console.log(' Headers:', JSON.stringify(req.headers, null, 2)); + console.log(' Body:', JSON.stringify(req.body, null, 2)); + + // Fix missing Accept headers for compatibility with Go MCP clients + // The StreamableHTTPServerTransport requires both application/json and text/event-stream + const accept = req.headers.accept || req.headers.Accept; + if ( + !accept || + !accept.includes('application/json') || + !accept.includes('text/event-stream') + ) { + console.log('Adding missing Accept headers for MCP compatibility'); + req.headers.accept = 'application/json, text/event-stream'; + } + + // Check for existing session ID + const sessionId = req.headers['mcp-session-id']; + let transport; + + if (sessionId && transports[sessionId]) { + // Reuse existing transport + transport = transports[sessionId]; + } else { + // Create new transport for new session + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (newSessionId) => { + console.log(`MCP session initialized: ${newSessionId}`); + // Store the transport by session ID + transports[newSessionId] = transport; + }, + }); + + // Clean up transport when closed + transport.onclose = () => { + if (transport.sessionId) { + console.log(`MCP session closed: ${transport.sessionId}`); + delete transports[transport.sessionId]; + } + }; + + // Create and connect MCP server + const server = createMcpServer(); + await server.connect(transport); + } + + // Handle the MCP request + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } +}); + +// Handle GET requests for SSE (server-to-client notifications) +app.get('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + const transport = transports[sessionId]; + await transport.handleRequest(req, res); +}); + +// Handle DELETE requests for session termination +app.delete('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + const transport = transports[sessionId]; + await transport.handleRequest(req, res); +}); + +// Health check endpoint +app.get('/health', (req, res) => { + const healthStatus = { + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + service: 'mcp-web-search', + version: '1.0.0', + protocol: 'Model Context Protocol', + transport: 'Streamable HTTP', + }; + + console.log('Health check requested:', healthStatus); + + res.json(healthStatus); +}); + +// Start the server +const port = process.env.PORT || 3001; +const host = process.env.HOST || '0.0.0.0'; + +app.listen(port, host, () => { + console.log(`MCP Web Search server running on http://${host}:${port}`); + console.log('Protocol: Model Context Protocol (MCP)'); + console.log('Transport: Streamable HTTP'); + console.log('Available endpoints:'); + console.log(' POST /mcp - MCP protocol endpoint'); + console.log( + ' GET /mcp - SSE notifications (with session-id header)' + ); + console.log( + ' DELETE /mcp - Session termination (with session-id header)' + ); + console.log(' GET /health - Health check'); + console.log('Available tools:'); + console.log(' - fetch_url - Fetch content from a URL'); + console.log(' - search_web - Perform web search (simulated)'); + console.log(' - get_page_title - Extract title from a web page'); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('Received SIGTERM, shutting down gracefully'); + // Close all transports + Object.values(transports).forEach((transport) => { + if (transport.close) transport.close(); + }); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.log('Received SIGINT, shutting down gracefully'); + // Close all transports + Object.values(transports).forEach((transport) => { + if (transport.close) transport.close(); + }); + process.exit(0); +}); diff --git a/examples/mcp/mcp-servers/web-search/package-lock.json b/examples/mcp/mcp-servers/web-search/package-lock.json new file mode 100644 index 0000000..c795960 --- /dev/null +++ b/examples/mcp/mcp-servers/web-search/package-lock.json @@ -0,0 +1,1798 @@ +{ + "name": "mcp-web-search-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-web-search-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "axios": "^1.6.0", + "cheerio": "^1.0.0-rc.12", + "cors": "^2.8.5", + "express": "^4.18.2", + "zod": "^3.22.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz", + "integrity": "sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz", + "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.30", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.30.tgz", + "integrity": "sha512-VolhdEtu6TJr/fzGuHA/SZ5ixvXqA6ADOG9VRcQ3rdOKmF5hkmcJbyaQjUH5BgmpA9gej++zYRX7zjSmdReIwA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/examples/mcp/mcp-servers/web-search/package.json b/examples/mcp/mcp-servers/web-search/package.json new file mode 100644 index 0000000..db7c2f3 --- /dev/null +++ b/examples/mcp/mcp-servers/web-search/package.json @@ -0,0 +1,24 @@ +{ + "name": "mcp-web-search-server", + "version": "1.0.0", + "description": "MCP Web Search Server for demonstration purposes", + "type": "module", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "axios": "^1.6.0", + "cheerio": "^1.0.0-rc.12", + "cors": "^2.8.5", + "express": "^4.18.2", + "zod": "^3.22.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "author": "Inference Gateway Team", + "license": "MIT" +} diff --git a/examples/mcp/package.json b/examples/mcp/package.json index 227308b..15712a6 100644 --- a/examples/mcp/package.json +++ b/examples/mcp/package.json @@ -6,6 +6,8 @@ "private": true, "scripts": { "start": "tsx index.ts", + "example:mcp:remotetools": "tsx example-mcp-tools.ts", + "example:mcp:withlocaltools": "tsx example-mcp-with-tools.ts", "compose:up": "docker-compose up -d", "compose:down": "docker-compose down", "compose:logs": "docker-compose logs -f" diff --git a/examples/mcp/shared/.gitignore b/examples/mcp/shared/.gitignore new file mode 100644 index 0000000..0c42a13 --- /dev/null +++ b/examples/mcp/shared/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!sample_sales_data.csv diff --git a/examples/mcp/shared/README.md b/examples/mcp/shared/README.md deleted file mode 100644 index a42e083..0000000 --- a/examples/mcp/shared/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Sample Data for MCP Example - -This directory contains sample files that can be accessed by the MCP filesystem server. - -## Files - -- `sample.txt` - A simple text file for testing file reading -- `data.json` - JSON data for testing structured file operations -- `config.yaml` - YAML configuration example - -## Usage - -These files can be accessed through MCP tools when the filesystem server is running. The AI models can read, analyze, and work with this data using the MCP file operations. diff --git a/examples/mcp/shared/config.yaml b/examples/mcp/shared/config.yaml deleted file mode 100644 index d41e170..0000000 --- a/examples/mcp/shared/config.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# MCP Configuration Example -# This YAML file demonstrates configuration data that can be read by MCP tools - -api: - version: 'v1' - base_url: 'http://localhost:8080' - timeout: 30s - -mcp: - enabled: true - expose: true - servers: - - name: filesystem - url: 'http://mcp-filesystem:3000/mcp' - timeout: 10s - - name: web-search - url: 'http://mcp-web-search:3001/mcp' - timeout: 15s - -providers: - groq: - api_url: 'https://api.groq.com/openai/v1' - models: - - 'meta-llama/llama-3.3-70b-versatile' - - 'meta-llama/llama-3.2-90b-vision-preview' - - openai: - api_url: 'https://api.openai.com/v1' - models: - - 'gpt-4o' - - 'gpt-4o-mini' - - 'gpt-3.5-turbo' - - anthropic: - api_url: 'https://api.anthropic.com/v1' - models: - - 'claude-3-5-sonnet-20241022' - - 'claude-3-5-haiku-20241022' - -logging: - level: info - format: json - -security: - cors_enabled: true - auth_required: false - allowed_origins: - - 'http://localhost:3000' - - 'http://localhost:8080' diff --git a/examples/mcp/shared/data.json b/examples/mcp/shared/data.json deleted file mode 100644 index dee631c..0000000 --- a/examples/mcp/shared/data.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "project": { - "name": "Inference Gateway TypeScript SDK", - "version": "0.7.1", - "description": "TypeScript SDK for the Inference Gateway", - "repository": "https://github.com/inference-gateway/typescript-sdk" - }, - "examples": { - "mcp": { - "description": "Model Context Protocol integration example", - "features": [ - "Tool discovery", - "File operations", - "Web scraping", - "Multi-tool conversations" - ], - "servers": [ - { - "name": "filesystem", - "port": 3000, - "tools": ["read_file", "write_file", "list_directory"] - }, - { - "name": "web-search", - "port": 3001, - "tools": ["fetch_url", "search_web"] - } - ] - } - }, - "providers": [ - "openai", - "groq", - "anthropic", - "ollama", - "cohere", - "deepseek", - "cloudflare" - ], - "created": "2025-05-27", - "purpose": "Demonstrate MCP capabilities with Inference Gateway" -} diff --git a/examples/mcp/shared/sample.txt b/examples/mcp/shared/sample.txt deleted file mode 100644 index 1b0bc79..0000000 --- a/examples/mcp/shared/sample.txt +++ /dev/null @@ -1,18 +0,0 @@ -Hello from the MCP filesystem server! - -This is a sample text file that demonstrates how MCP tools can interact with the file system. - -The file was created as part of the Inference Gateway TypeScript SDK examples. - -You can ask the AI assistant to: -- Read this file's contents -- Analyze the text -- Summarize the information -- Count words or lines -- Extract specific information - -The MCP filesystem server allows secure access to this shared directory, -enabling AI models to work with real file data through the Model Context Protocol. - -Created: May 27, 2025 -Purpose: Demonstration and testing of MCP file operations diff --git a/examples/mcp/shared/sample_sales_data.csv b/examples/mcp/shared/sample_sales_data.csv new file mode 100644 index 0000000..628aa2d --- /dev/null +++ b/examples/mcp/shared/sample_sales_data.csv @@ -0,0 +1,16 @@ +Date,Product,Quantity,Revenue,Region +2025-01-01,Laptop,10,15000,North +2025-01-02,Mouse,25,500,South +2025-01-03,Keyboard,15,750,East +2025-01-04,Monitor,8,2400,West +2025-01-05,Laptop,12,18000,North +2025-01-06,Mouse,30,600,South +2025-01-07,Keyboard,20,1000,East +2025-01-08,Monitor,6,1800,West +2025-01-09,Laptop,8,12000,Central +2025-01-10,Mouse,35,700,North +2025-01-11,Keyboard,25,1250,South +2025-01-12,Monitor,10,3000,East +2025-01-13,Laptop,15,22500,West +2025-01-14,Mouse,40,800,Central +2025-01-15,Keyboard,18,900,North diff --git a/src/client.ts b/src/client.ts index fec69f1..3ce770d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,7 +11,7 @@ import type { } from './types/generated'; import { ChatCompletionToolType } from './types/generated'; -interface ChatCompletionStreamCallbacks { +export interface ChatCompletionStreamCallbacks { onOpen?: () => void; onChunk?: (chunk: SchemaCreateChatCompletionStreamResponse) => void; onReasoning?: (reasoningContent: string) => void; @@ -22,6 +22,216 @@ interface ChatCompletionStreamCallbacks { response: SchemaCreateChatCompletionStreamResponse | null ) => void; onError?: (error: SchemaError) => void; + onMCPTool?: (toolCall: SchemaChatCompletionMessageToolCall) => void; +} + +/** + * Handles streaming response processing with enhanced support for MCP and tool calls + */ +class StreamProcessor { + private callbacks: ChatCompletionStreamCallbacks; + private clientProvidedTools: Set; + private incompleteToolCalls = new Map< + number, + { + id: string; + type: ChatCompletionToolType; + function: { + name: string; + arguments: string; + }; + } + >(); + + constructor( + callbacks: ChatCompletionStreamCallbacks, + clientProvidedTools: Set + ) { + this.callbacks = callbacks; + this.clientProvidedTools = clientProvidedTools; + } + + async processStream(body: ReadableStream): Promise { + const reader = body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(5).trim(); + await this.processSSEData(data); + } + } + } + } catch (error) { + const apiError: SchemaError = { + error: (error as Error).message || 'Unknown error', + }; + this.callbacks.onError?.(apiError); + throw error; + } finally { + try { + reader.releaseLock(); + } catch { + // Reader might already be closed, ignore + } + } + } + + private async processSSEData(data: string): Promise { + if (data === '[DONE]') { + this.finalizeIncompleteToolCalls(); + this.callbacks.onFinish?.(null); + return; + } + + try { + const chunk: SchemaCreateChatCompletionStreamResponse = JSON.parse(data); + this.callbacks.onChunk?.(chunk); + + if (chunk.usage && this.callbacks.onUsageMetrics) { + this.callbacks.onUsageMetrics(chunk.usage); + } + + const choice = chunk.choices?.[0]; + if (!choice) return; + + this.handleReasoningContent(choice); + + const content = choice.delta?.content; + if (content) { + this.callbacks.onContent?.(content); + } + + this.handleToolCalls(choice); + + this.handleFinishReason(choice); + } catch (parseError) { + const errorMessage = `Failed to parse SSE data: ${(parseError as Error).message}`; + globalThis.console.error(errorMessage, { data, parseError }); + + const apiError: SchemaError = { + error: errorMessage, + }; + this.callbacks.onError?.(apiError); + } + } + + private handleReasoningContent(choice: { + delta?: { reasoning_content?: string; reasoning?: string }; + }): void { + const reasoningContent = choice.delta?.reasoning_content; + if (reasoningContent !== undefined) { + this.callbacks.onReasoning?.(reasoningContent); + } + + const reasoning = choice.delta?.reasoning; + if (reasoning !== undefined) { + this.callbacks.onReasoning?.(reasoning); + } + } + + private handleToolCalls(choice: { + delta?: { + tool_calls?: Array<{ + index: number; + id?: string; + function?: { name?: string; arguments?: string }; + }>; + }; + }): void { + const toolCalls = choice.delta?.tool_calls; + if (!toolCalls || toolCalls.length === 0) return; + + for (const toolCallChunk of toolCalls) { + const index = toolCallChunk.index; + + if (!this.incompleteToolCalls.has(index)) { + this.incompleteToolCalls.set(index, { + id: toolCallChunk.id || '', + type: ChatCompletionToolType.function, + function: { + name: toolCallChunk.function?.name || '', + arguments: toolCallChunk.function?.arguments || '', + }, + }); + } else { + const existingToolCall = this.incompleteToolCalls.get(index)!; + + if (toolCallChunk.id) { + existingToolCall.id = toolCallChunk.id; + } + + if (toolCallChunk.function?.name) { + existingToolCall.function.name = toolCallChunk.function.name; + } + + if (toolCallChunk.function?.arguments) { + existingToolCall.function.arguments += + toolCallChunk.function.arguments; + } + } + } + } + + private handleFinishReason(choice: { finish_reason?: string }): void { + const finishReason = choice.finish_reason; + if (finishReason === 'tool_calls' && this.incompleteToolCalls.size > 0) { + this.finalizeIncompleteToolCalls(); + } + } + + private finalizeIncompleteToolCalls(): void { + for (const [, toolCall] of this.incompleteToolCalls.entries()) { + if (!toolCall.id || !toolCall.function.name) { + globalThis.console.warn('Incomplete tool call detected:', toolCall); + continue; + } + + const completedToolCall = { + id: toolCall.id, + type: toolCall.type, + function: { + name: toolCall.function.name, + arguments: toolCall.function.arguments, + }, + }; + + if (this.isMCPTool(toolCall.function.name)) { + try { + if (toolCall.function.arguments) { + JSON.parse(toolCall.function.arguments); + } + this.callbacks.onMCPTool?.(completedToolCall); + } catch (argError) { + globalThis.console.warn( + `Invalid MCP tool arguments for ${toolCall.function.name}:`, + argError + ); + } + } else { + this.callbacks.onTool?.(completedToolCall); + } + } + this.incompleteToolCalls.clear(); + } + + private isMCPTool(toolName: string): boolean { + if (!toolName || typeof toolName !== 'string') { + return false; + } + + return !this.clientProvidedTools.has(toolName); + } } export interface ClientOptions { @@ -179,6 +389,53 @@ export class InferenceGatewayClient { callbacks: ChatCompletionStreamCallbacks, provider?: Provider ): Promise { + try { + const response = await this.initiateStreamingRequest(request, provider); + + if (!response.body) { + const error: SchemaError = { + error: 'Response body is not readable', + }; + callbacks.onError?.(error); + throw new Error('Response body is not readable'); + } + + callbacks.onOpen?.(); + + // Extract tool names from client-provided tools + const clientProvidedTools = new Set(); + if (request.tools) { + for (const tool of request.tools) { + if (tool.type === 'function' && tool.function?.name) { + clientProvidedTools.add(tool.function.name); + } + } + } + + const streamProcessor = new StreamProcessor( + callbacks, + clientProvidedTools + ); + await streamProcessor.processStream(response.body); + } catch (error) { + const apiError: SchemaError = { + error: (error as Error).message || 'Unknown error occurred', + }; + callbacks.onError?.(apiError); + throw error; + } + } + + /** + * Initiates a streaming request to the chat completions endpoint + */ + private async initiateStreamingRequest( + request: Omit< + SchemaCreateChatCompletionRequest, + 'stream' | 'stream_options' + >, + provider?: Provider + ): Promise { const query: Record = {}; if (provider) { query.provider = provider; @@ -190,7 +447,9 @@ export class InferenceGatewayClient { }); const queryString = queryParams.toString(); - const url = `${this.baseURL}/chat/completions${queryString ? `?${queryString}` : ''}`; + const url = `${this.baseURL}/chat/completions${ + queryString ? `?${queryString}` : '' + }`; const headers = new Headers({ 'Content-Type': 'application/json', @@ -222,149 +481,17 @@ export class InferenceGatewayClient { }); if (!response.ok) { - const error: SchemaError = await response.json(); - throw new Error( - error.error || `HTTP error! status: ${response.status}` - ); - } - - if (!response.body) { - throw new Error('Response body is not readable'); - } - - callbacks.onOpen?.(); - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - - const incompleteToolCalls = new Map< - number, - { - id: string; - type: ChatCompletionToolType; - function: { - name: string; - arguments: string; - }; - } - >(); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - if (line.startsWith('data: ')) { - const data = line.slice(5).trim(); - - if (data === '[DONE]') { - for (const [, toolCall] of incompleteToolCalls.entries()) { - callbacks.onTool?.({ - id: toolCall.id, - type: toolCall.type, - function: { - name: toolCall.function.name, - arguments: toolCall.function.arguments, - }, - }); - } - callbacks.onFinish?.(null); - return; - } - - try { - const chunk: SchemaCreateChatCompletionStreamResponse = - JSON.parse(data); - callbacks.onChunk?.(chunk); - - if (chunk.usage && callbacks.onUsageMetrics) { - callbacks.onUsageMetrics(chunk.usage); - } - - const reasoning_content = - chunk.choices[0]?.delta?.reasoning_content; - if (reasoning_content !== undefined) { - callbacks.onReasoning?.(reasoning_content); - } - - const reasoning = chunk.choices[0]?.delta?.reasoning; - if (reasoning !== undefined) { - callbacks.onReasoning?.(reasoning); - } - - const content = chunk.choices[0]?.delta?.content; - if (content) { - callbacks.onContent?.(content); - } - - const toolCalls = chunk.choices[0]?.delta?.tool_calls; - if (toolCalls && toolCalls.length > 0) { - for (const toolCallChunk of toolCalls) { - const index = toolCallChunk.index; - - if (!incompleteToolCalls.has(index)) { - incompleteToolCalls.set(index, { - id: toolCallChunk.id || '', - type: ChatCompletionToolType.function, - function: { - name: toolCallChunk.function?.name || '', - arguments: toolCallChunk.function?.arguments || '', - }, - }); - } else { - const existingToolCall = incompleteToolCalls.get(index)!; - - if (toolCallChunk.id) { - existingToolCall.id = toolCallChunk.id; - } - - if (toolCallChunk.function?.name) { - existingToolCall.function.name = - toolCallChunk.function.name; - } - - if (toolCallChunk.function?.arguments) { - existingToolCall.function.arguments += - toolCallChunk.function.arguments; - } - } - } - } - - const finishReason = chunk.choices[0]?.finish_reason; - if ( - finishReason === 'tool_calls' && - incompleteToolCalls.size > 0 - ) { - for (const [, toolCall] of incompleteToolCalls.entries()) { - callbacks.onTool?.({ - id: toolCall.id, - type: toolCall.type, - function: { - name: toolCall.function.name, - arguments: toolCall.function.arguments, - }, - }); - } - incompleteToolCalls.clear(); - } - } catch (e) { - globalThis.console.error('Error parsing SSE data:', e); - } - } + let errorMessage = `HTTP error! status: ${response.status}`; + try { + const error: SchemaError = await response.json(); + errorMessage = error.error || errorMessage; + } catch { + // Failed to parse error response as JSON, use status message } + throw new Error(errorMessage); } - } catch (error) { - const apiError: SchemaError = { - error: (error as Error).message || 'Unknown error', - }; - callbacks.onError?.(apiError); - throw error; + + return response; } finally { globalThis.clearTimeout(timeoutId); } From 3ea92c1c52113e0e36e7ae74a958e08dbef7bcf9 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Fri, 30 May 2025 00:12:10 +0000 Subject: [PATCH 09/38] docs(examples): Refactor MCP examples and client for improved error handling and token tracking - Updated docker-compose.yml to set environment to production and commented out optional Ollama service. - Enhanced example-mcp-tools.ts to use console.info for logging and added model availability checks. - Modified index.ts to implement token tracking for prompt and completion tokens, displaying usage metrics at the end. - Improved filesystem MCP server (index.js) to use console.info for logging and ensure consistent messaging. - Updated web-search MCP server (index-http.js and index.js) to use console.info for logging and improved request handling. - Enhanced InferenceGatewayClient to handle mid-stream errors and added type definitions for stream chunks that may contain errors. Signed-off-by: Eden Reich --- examples/mcp/docker-compose.yml | 29 +- examples/mcp/example-mcp-tools.ts | 105 +++++--- examples/mcp/index.ts | 247 +++++++++++++----- examples/mcp/mcp-servers/filesystem/index.js | 70 ++--- .../mcp/mcp-servers/web-search/index-http.js | 48 ++-- examples/mcp/mcp-servers/web-search/index.js | 48 ++-- src/client.ts | 43 ++- 7 files changed, 385 insertions(+), 205 deletions(-) diff --git a/examples/mcp/docker-compose.yml b/examples/mcp/docker-compose.yml index 1db24fe..c3bb0c2 100644 --- a/examples/mcp/docker-compose.yml +++ b/examples/mcp/docker-compose.yml @@ -5,7 +5,7 @@ services: - '8080:8080' environment: # General settings - ENVIRONMENT: development + ENVIRONMENT: production # Enable MCP support MCP_ENABLE: 'true' @@ -39,8 +39,6 @@ services: condition: service_healthy networks: - inference-network - volumes: - - shared-data:/shared healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:8080/health'] interval: 30s @@ -70,7 +68,6 @@ services: networks: - inference-network volumes: - - shared-data:/shared - ./shared:/tmp healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:3000/health'] @@ -106,18 +103,18 @@ services: start_period: 45s restart: unless-stopped - # Optional: Ollama for local models - ollama: - image: ollama/ollama:latest - ports: - - '11434:11434' - networks: - - inference-network - volumes: - - ollama-data:/root/.ollama - environment: - OLLAMA_HOST: '0.0.0.0' - restart: unless-stopped + # # Optional: Ollama for local models + # ollama: + # image: ollama/ollama:latest + # ports: + # - '11434:11434' + # networks: + # - inference-network + # volumes: + # - ollama-data:/root/.ollama + # environment: + # OLLAMA_HOST: '0.0.0.0' + # restart: unless-stopped volumes: shared-data: diff --git a/examples/mcp/example-mcp-tools.ts b/examples/mcp/example-mcp-tools.ts index 0b0a03a..23611a7 100644 --- a/examples/mcp/example-mcp-tools.ts +++ b/examples/mcp/example-mcp-tools.ts @@ -17,45 +17,74 @@ import { const provider = (process.env.PROVIDER as Provider) || Provider.groq; const model = process.env.LLM || 'llama-3.3-70b-versatile'; - console.log(`Using model: ${model}`); - console.log(`Using provider: ${provider}\n`); + console.info(`Using model: ${model}`); + console.info(`Using provider: ${provider}\n`); - console.log('=== MCP Tool Usage Demo ===\n'); + console.info('=== MCP Tool Usage Demo ===\n'); try { // Check gateway health - console.log('🔍 Checking gateway health...'); + console.info('🔍 Checking gateway health...'); const isHealthy = await client.healthCheck(); - console.log( + console.info( `Gateway health: ${isHealthy ? '✅ Healthy' : '❌ Unhealthy'}\n` ); if (!isHealthy) { - console.log( + console.info( 'Please ensure the Inference Gateway is running with Docker Compose.' ); process.exit(1); } + // List available models for the provider + console.info('🔍 Checking available models...'); + try { + const models = await client.listModels(provider); + console.info(`Found ${models.data.length} models for ${provider}:`); + models.data.forEach((modelInfo, index) => { + console.info(`${index + 1}. ${modelInfo.id}`); + }); + + // Check if the requested model is available + const isModelAvailable = models.data.some( + (m) => m.id === `${provider}/${model}` + ); + if (!isModelAvailable) { + console.info( + `⚠️ Model '${model}' not found for provider '${provider}'` + ); + console.info( + 'Consider using one of the available models listed above.' + ); + console.info( + 'You can set the LLM environment variable to use a different model.\n' + ); + } + } catch (modelError) { + console.info('⚠️ Could not retrieve model list:', modelError); + } + console.info(''); + // List available MCP tools - console.log('📋 Listing available MCP tools...'); + console.info('📋 Listing available MCP tools...'); const tools = await client.listTools(); - console.log(`Found ${tools.data.length} MCP tools:\n`); + console.info(`Found ${tools.data.length} MCP tools:\n`); const fileTools = tools.data.filter((tool) => ['read_file', 'write_file', 'list_directory'].includes(tool.name) ); if (fileTools.length === 0) { - console.log('⚠️ No filesystem MCP tools available.'); + console.info('⚠️ No filesystem MCP tools available.'); return; } - console.log('📁 Available filesystem tools:'); + console.info('📁 Available filesystem tools:'); fileTools.forEach((tool, index) => { - console.log(`${index + 1}. ${tool.name} - ${tool.description}`); + console.info(`${index + 1}. ${tool.name} - ${tool.description}`); }); - console.log(''); + console.info(''); // Track MCP tool calls for demonstration const toolCallTracker = { @@ -65,7 +94,7 @@ import { }; // Example: Analyze highest revenue from sales data - console.log('=== Highest Revenue Analysis with MCP Tool Tracking ===\n'); + console.info('=== Highest Revenue Analysis with MCP Tool Tracking ===\n'); await client.streamChatCompletion( { @@ -85,15 +114,15 @@ import { }, { onOpen: () => { - console.log('🚀 Starting revenue analysis...'); - console.log('📊 MCP Tool usage will be tracked below:\n'); + console.info('🚀 Starting revenue analysis...'); + console.info('📊 MCP Tool usage will be tracked below:\n'); }, onContent: (content) => process.stdout.write(content), onMCPTool: (toolCall) => { toolCallTracker.totalCalls++; toolCallTracker.toolsUsed.add(toolCall.function.name); - console.log( + console.info( `\n🔧 [TOOL CALL #${toolCallTracker.totalCalls}] ${toolCall.function.name}` ); @@ -101,12 +130,12 @@ import { switch (toolCall.function.name) { case 'read_file': - console.log(` 📄 Reading file: ${args.path}`); + console.info(` 📄 Reading file: ${args.path}`); toolCallTracker.filesAccessed.add(args.path); break; case 'write_file': - console.log(` 💾 Writing file: ${args.path}`); - console.log( + console.info(` 💾 Writing file: ${args.path}`); + console.info( ` 📝 Content length: ${ args.content ? args.content.length : 0 } characters` @@ -114,49 +143,51 @@ import { toolCallTracker.filesAccessed.add(args.path); break; case 'list_directory': - console.log(` 📂 Listing directory: ${args.path}`); + console.info(` 📂 Listing directory: ${args.path}`); break; default: - console.log(` ⚙️ Arguments: ${JSON.stringify(args, null, 2)}`); + console.info( + ` ⚙️ Arguments: ${JSON.stringify(args, null, 2)}` + ); } - console.log(` 🆔 Tool ID: ${toolCall.id}`); + console.info(` 🆔 Tool ID: ${toolCall.id}`); }, onFinish: () => { - console.log('\n\n✅ Revenue analysis completed!\n'); + console.info('\n\n✅ Revenue analysis completed!\n'); // Display tool usage summary - console.log('📈 MCP Tool Usage Summary:'); - console.log(` Total tool calls: ${toolCallTracker.totalCalls}`); - console.log( + console.info('📈 MCP Tool Usage Summary:'); + console.info(` Total tool calls: ${toolCallTracker.totalCalls}`); + console.info( ` Tools used: ${Array.from(toolCallTracker.toolsUsed).join(', ')}` ); - console.log( + console.info( ` Files accessed: ${Array.from( toolCallTracker.filesAccessed ).join(', ')}` ); - console.log(''); + console.info(''); }, onError: (error) => console.error('❌ Error:', error), }, provider ); - console.log('🎉 MCP Tool Usage Demo completed successfully!'); - console.log('\n💡 Key takeaways:'); - console.log( + console.info('🎉 MCP Tool Usage Demo completed successfully!'); + console.info('\n💡 Key takeaways:'); + console.info( '- The onMCPTool callback provides detailed tracking of tool usage' ); - console.log( + console.info( '- Track total tool calls, tool types used, and files accessed' ); - console.log( + console.info( '- Each tool call includes function name, arguments, and unique ID' ); - console.log( + console.info( '- Perfect for debugging, monitoring, and understanding AI tool usage patterns' ); - console.log( + console.info( '- LLM can read CSV data and perform complex analysis with file operations\n' ); } catch (error) { @@ -167,8 +198,8 @@ import { console.error( '❌ MCP tools are not exposed. Please ensure the Inference Gateway is started with MCP_EXPOSE=true' ); - console.log('\n💡 To fix this, restart the gateway with:'); - console.log(' docker-compose up --build'); + console.info('\n💡 To fix this, restart the gateway with:'); + console.info(' docker-compose up --build'); } else { console.error('❌ Error:', error); } diff --git a/examples/mcp/index.ts b/examples/mcp/index.ts index 4b915c0..85eecc7 100644 --- a/examples/mcp/index.ts +++ b/examples/mcp/index.ts @@ -4,6 +4,14 @@ import { Provider, } from '../../src/index.js'; +// Token tracking interface +interface TokenTracker { + totalPromptTokens: number; + totalCompletionTokens: number; + totalTokens: number; + requestCount: number; +} + (async () => { const client = new InferenceGatewayClient({ baseURL: 'http://localhost:8080/v1', @@ -12,48 +20,87 @@ import { const provider = (process.env.PROVIDER as Provider) || Provider.groq; const model = process.env.LLM || 'llama-3.3-70b-versatile'; - console.log(`Using model: ${model}`); - console.log(`Using provider: ${provider}\n`); + // Initialize token tracker + const tokenTracker: TokenTracker = { + totalPromptTokens: 0, + totalCompletionTokens: 0, + totalTokens: 0, + requestCount: 0, + }; + + // Helper function to update token tracking + const updateTokens = (usage: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }) => { + tokenTracker.totalPromptTokens += usage.prompt_tokens; + tokenTracker.totalCompletionTokens += usage.completion_tokens; + tokenTracker.totalTokens += usage.total_tokens; + tokenTracker.requestCount++; + }; + + // Helper function to display current token usage + const displayTokenUsage = (label: string) => { + console.info(`\n💰 Token Usage for ${label}:`); + console.info( + ` 📊 Prompt tokens: ${tokenTracker.totalPromptTokens.toLocaleString()}` + ); + console.info( + ` ✍️ Completion tokens: ${tokenTracker.totalCompletionTokens.toLocaleString()}` + ); + console.info( + ` 🎯 Total tokens: ${tokenTracker.totalTokens.toLocaleString()}` + ); + console.info( + ` 📈 Average tokens per request: ${Math.round( + tokenTracker.totalTokens / Math.max(tokenTracker.requestCount, 1) + ).toLocaleString()}` + ); + }; + + console.info(`Using model: ${model}`); + console.info(`Using provider: ${provider}\n`); - console.log('=== MCP Tools Example ===\n'); + console.info('=== MCP Tools Example with Token Tracking ===\n'); try { // First, let's check if the gateway is healthy - console.log('🔍 Checking gateway health...'); + console.info('🔍 Checking gateway health...'); const isHealthy = await client.healthCheck(); - console.log( + console.info( `Gateway health: ${isHealthy ? '✅ Healthy' : '❌ Unhealthy'}\n` ); if (!isHealthy) { - console.log( + console.info( 'Please ensure the Inference Gateway is running with Docker Compose.' ); process.exit(1); } // List available MCP tools - console.log('📋 Listing available MCP tools...'); + console.info('📋 Listing available MCP tools...'); const tools = await client.listTools(); - console.log(`Found ${tools.data.length} MCP tools:\n`); + console.info(`Found ${tools.data.length} MCP tools:\n`); - tools.data.forEach((tool, index) => { - console.log(`${index + 1}. ${tool.name}`); - console.log(` Description: ${tool.description}`); - console.log(` Server: ${tool.server}`); - console.log(` Schema: ${JSON.stringify(tool.input_schema, null, 2)}\n`); - }); + // tools.data.forEach((tool, index) => { + // console.info(`${index + 1}. ${tool.name}`); + // console.info(` Description: ${tool.description}`); + // console.info(` Server: ${tool.server}`); + // console.info(` Schema: ${JSON.stringify(tool.input_schema, null, 2)}\n`); + // }); if (tools.data.length === 0) { - console.log( + console.info( '⚠️ No MCP tools available. Ensure MCP servers are configured and running.' ); return; } // Example 0: Simple test without tools first - console.log('=== Example 0: Simple Test (No Tools) ===\n'); - console.log('Testing basic streaming without tools first...\n'); + console.info('=== Example 0: Simple Test (No Tools) ===\n'); + console.info('Testing basic streaming without tools first...\n'); await client.streamChatCompletion( { @@ -67,14 +114,19 @@ import { max_tokens: 50, }, { - onOpen: () => console.log('🚀 Starting simple test...'), + onOpen: () => console.info('🚀 Starting simple test...'), onContent: (content) => process.stdout.write(content), onTool: (toolCall) => { - console.log(`\n🔧 Tool called: ${toolCall.function.name}`); - console.log(`📝 Arguments: ${toolCall.function.arguments}`); + console.info(`\n🔧 Tool called: ${toolCall.function.name}`); + console.info(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onUsageMetrics: (usage) => { + updateTokens(usage); }, onFinish: () => { - console.log('\n✅ Simple test completed\n'); + console.info('\n✅ Simple test completed'); + displayTokenUsage('Simple Test'); + console.info(''); }, onError: (error) => console.error('❌ Error:', error), }, @@ -82,8 +134,8 @@ import { ); // Example 1: Automatic tool discovery and usage - console.log('=== Example 1: Automatic Tool Discovery ===\n'); - console.log( + console.info('=== Example 1: Automatic Tool Discovery ===\n'); + console.info( 'The gateway automatically detects and uses available MCP tools based on context.\n' ); @@ -105,16 +157,22 @@ import { max_tokens: 200, }, { - onOpen: () => console.log('🚀 Starting automatic tool discovery...'), + onOpen: () => console.info('🚀 Starting automatic tool discovery...'), onContent: (content) => process.stdout.write(content), onTool: (toolCall) => { - console.log( + console.info( `\n🔧 Tool automatically called: ${toolCall.function.name}` ); - console.log(`📝 Arguments: ${toolCall.function.arguments}`); + console.info(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onUsageMetrics: (usage) => { + updateTokens(usage); + }, + onFinish: () => { + console.info('\n✅ Automatic tool discovery completed'); + displayTokenUsage('Automatic Tool Discovery'); + console.info(''); }, - onFinish: () => - console.log('\n✅ Automatic tool discovery completed\n'), onError: (error) => console.error('❌ Error:', error), }, provider @@ -123,7 +181,7 @@ import { // Example 2: Use MCP tools for file operations (if filesystem MCP server is available) const fileReadTool = tools.data.find((tool) => tool.name === 'read_file'); if (fileReadTool) { - console.log('=== Example 2: File Operations with MCP ===\n'); + console.info('=== Example 2: File Operations with MCP ===\n'); await client.streamChatCompletion( { @@ -132,24 +190,31 @@ import { { role: MessageRole.system, content: - 'You are a helpful assistant with access to filesystem operations. Available directories are /shared and /tmp.', + 'You are a helpful assistant with access to filesystem operations. Available directory is /tmp.', }, { role: MessageRole.user, content: - 'Can you read the contents of /shared/mcp-filesystem-example.txt and tell me what it contains?', + 'Can you read the contents of /tmp/mcp-filesystem-example.txt and tell me what it contains?', }, ], max_tokens: 200, }, { - onOpen: () => console.log('🚀 Starting file reading example...'), + onOpen: () => console.info('🚀 Starting file reading example...'), onContent: (content) => process.stdout.write(content), onTool: (toolCall) => { - console.log(`\n🔧 Tool called: ${toolCall.function.name}`); - console.log(`📝 Arguments: ${toolCall.function.arguments}`); + console.info(`\n🔧 Tool called: ${toolCall.function.name}`); + console.info(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onUsageMetrics: (usage) => { + updateTokens(usage); + }, + onFinish: () => { + console.info('\n✅ File reading example completed'); + displayTokenUsage('File Reading Example'); + console.info(''); }, - onFinish: () => console.log('\n✅ File reading example completed\n'), onError: (error) => console.error('❌ Error:', error), }, provider @@ -161,7 +226,7 @@ import { (tool) => tool.name.includes('fetch') || tool.name.includes('scrape') ); if (webScrapeTool) { - console.log('=== Example 3: Web Scraping with MCP ===\n'); + console.info('=== Example 3: Web Scraping with MCP ===\n'); await client.streamChatCompletion( { @@ -181,13 +246,20 @@ import { max_tokens: 200, }, { - onOpen: () => console.log('🚀 Starting web scraping example...'), + onOpen: () => console.info('🚀 Starting web scraping example...'), onContent: (content) => process.stdout.write(content), onTool: (toolCall) => { - console.log(`\n🔧 Tool called: ${toolCall.function.name}`); - console.log(`📝 Arguments: ${toolCall.function.arguments}`); + console.info(`\n🔧 Tool called: ${toolCall.function.name}`); + console.info(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onUsageMetrics: (usage) => { + updateTokens(usage); + }, + onFinish: () => { + console.info('\n✅ Web scraping example completed'); + displayTokenUsage('Web Scraping Example'); + console.info(''); }, - onFinish: () => console.log('\n✅ Web scraping example completed\n'), onError: (error) => console.error('❌ Error:', error), }, provider @@ -196,10 +268,10 @@ import { // Example 4: Generic MCP tool usage - use the first available tool if (tools.data.length > 0 && !fileReadTool && !webScrapeTool) { - console.log('=== Example 4: Generic MCP Tool Usage ===\n'); + console.info('=== Example 4: Generic MCP Tool Usage ===\n'); const firstTool = tools.data[0]; - console.log(`Using tool: ${firstTool.name}\n`); + console.info(`Using tool: ${firstTool.name}\n`); await client.streamChatCompletion( { @@ -217,13 +289,20 @@ import { max_tokens: 200, }, { - onOpen: () => console.log('🚀 Starting generic tool example...'), + onOpen: () => console.info('🚀 Starting generic tool example...'), onContent: (content) => process.stdout.write(content), onTool: (toolCall) => { - console.log(`\n🔧 Tool called: ${toolCall.function.name}`); - console.log(`📝 Arguments: ${toolCall.function.arguments}`); + console.info(`\n🔧 Tool called: ${toolCall.function.name}`); + console.info(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onUsageMetrics: (usage) => { + updateTokens(usage); + }, + onFinish: () => { + console.info('\n✅ Generic tool example completed'); + displayTokenUsage('Generic Tool Example'); + console.info(''); }, - onFinish: () => console.log('\n✅ Generic tool example completed\n'), onError: (error) => console.error('❌ Error:', error), }, provider @@ -232,7 +311,7 @@ import { // Example 5: Data Analysis with File Operations if (tools.data.length > 1) { - console.log('=== Example 5: Data Analysis with File Operations ===\n'); + console.info('=== Example 5: Data Analysis with File Operations ===\n'); await client.streamChatCompletion( { @@ -240,24 +319,31 @@ import { messages: [ { role: MessageRole.system, - content: `You are a helpful data analysis assistant with access to filesystem tools. Available directories are /shared and /tmp. You can read, write, and analyze files. The /shared directory contains sample data files for analysis.`, + content: `You are a helpful data analysis assistant with access to filesystem tools. Available directory is /tmp. You can read, write, and analyze files. The /tmp directory contains sample data files for analysis.`, }, { role: MessageRole.user, content: - 'I need help with data analysis. First, can you check what files are available in the /shared directory? Then create a simple CSV file with sample sales data in /tmp/sales_data.csv and analyze it.', + 'I need help with data analysis. First, can you check what files are available in the /tmp directory? Then create a simple CSV file with sample sales data in /tmp/sales_data.csv and analyze it.', }, ], max_tokens: 400, }, { - onOpen: () => console.log('🚀 Starting data analysis example...'), + onOpen: () => console.info('🚀 Starting data analysis example...'), onContent: (content) => process.stdout.write(content), onTool: (toolCall) => { - console.log(`\n🔧 Tool called: ${toolCall.function.name}`); - console.log(`📝 Arguments: ${toolCall.function.arguments}`); + console.info(`\n🔧 Tool called: ${toolCall.function.name}`); + console.info(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onUsageMetrics: (usage) => { + updateTokens(usage); + }, + onFinish: () => { + console.info('\n✅ Data analysis example completed'); + displayTokenUsage('Data Analysis Example'); + console.info(''); }, - onFinish: () => console.log('\n✅ Data analysis example completed\n'), onError: (error) => console.error('❌ Error:', error), }, provider @@ -265,7 +351,7 @@ import { } // Example 6: File Creation and Manipulation - console.log('=== Example 6: File Creation and Manipulation ===\n'); + console.info('=== Example 6: File Creation and Manipulation ===\n'); await client.streamChatCompletion( { @@ -273,7 +359,7 @@ import { messages: [ { role: MessageRole.system, - content: `You are a helpful assistant with filesystem access. Available directories are /shared and /tmp. You can create, read, write, and manage files in these directories.`, + content: `You are a helpful assistant with filesystem access. Available directory is /tmp. You can create, read, write, and manage files in this directory.`, }, { role: MessageRole.user, @@ -284,14 +370,21 @@ import { max_tokens: 300, }, { - onOpen: () => console.log('🚀 Starting file manipulation example...'), + onOpen: () => console.info('🚀 Starting file manipulation example...'), + onReasoning: (content) => process.stdout.write(content), onContent: (content) => process.stdout.write(content), onTool: (toolCall) => { - console.log(`\n🔧 Tool called: ${toolCall.function.name}`); - console.log(`📝 Arguments: ${toolCall.function.arguments}`); + console.info(`\n🔧 Tool called: ${toolCall.function.name}`); + console.info(`📝 Arguments: ${toolCall.function.arguments}`); + }, + onUsageMetrics: (usage) => { + updateTokens(usage); + }, + onFinish: () => { + console.info('\n✅ File manipulation example completed'); + displayTokenUsage('File Manipulation Example'); + console.info(''); }, - onFinish: () => - console.log('\n✅ File manipulation example completed\n'), onError: (error) => console.error('❌ Error:', error), }, provider @@ -304,10 +397,40 @@ import { console.error( '❌ MCP tools are not exposed. Please ensure the Inference Gateway is started with EXPOSE_MCP=true' ); - console.log('\n💡 To fix this, restart the gateway with:'); - console.log(' docker-compose up --build'); + console.info('\n💡 To fix this, restart the gateway with:'); + console.info(' docker-compose up --build'); } else { console.error('❌ Error:', error); } + } finally { + // Display final token summary + console.info('\n' + '='.repeat(60)); + console.info('📊 FINAL TOKEN USAGE SUMMARY'); + console.info('='.repeat(60)); + console.info(`🔢 Total Requests: ${tokenTracker.requestCount}`); + console.info( + `📊 Total Prompt Tokens: ${tokenTracker.totalPromptTokens.toLocaleString()}` + ); + console.info( + `✍️ Total Completion Tokens: ${tokenTracker.totalCompletionTokens.toLocaleString()}` + ); + console.info( + `🎯 Total Tokens Used: ${tokenTracker.totalTokens.toLocaleString()}` + ); + + if (tokenTracker.requestCount > 0) { + console.info( + `📈 Average Tokens per Request: ${Math.round( + tokenTracker.totalTokens / tokenTracker.requestCount + ).toLocaleString()}` + ); + } + + // Calculate cost estimate (example rates - adjust based on actual provider pricing) + const estimatedCost = tokenTracker.totalTokens * 0.000001; // Example: $0.000001 per token + console.info( + `💰 Estimated Cost: $${estimatedCost.toFixed(6)} (example rate)` + ); + console.info('='.repeat(60)); } })(); diff --git a/examples/mcp/mcp-servers/filesystem/index.js b/examples/mcp/mcp-servers/filesystem/index.js index f1fda76..8f597e8 100644 --- a/examples/mcp/mcp-servers/filesystem/index.js +++ b/examples/mcp/mcp-servers/filesystem/index.js @@ -26,7 +26,7 @@ const allowedDirectories = ( process.env.ALLOWED_DIRECTORIES || '/shared,/tmp' ).split(','); -console.log('Allowed directories:', allowedDirectories); +console.info('Allowed directories:', allowedDirectories); /** * Check if a path is within allowed directories @@ -62,7 +62,7 @@ function createMcpServer() { } try { - console.log(`Reading file: ${filePath}`); + console.info(`Reading file: ${filePath}`); const content = await fs.readFile(filePath, 'utf8'); return { @@ -114,7 +114,7 @@ function createMcpServer() { } try { - console.log(`Writing to file: ${filePath}`); + console.info(`Writing to file: ${filePath}`); // Ensure directory exists const dir = path.dirname(filePath); @@ -159,7 +159,7 @@ function createMcpServer() { } try { - console.log(`Listing directory: ${dirPath}`); + console.info(`Listing directory: ${dirPath}`); const entries = await fs.readdir(dirPath, { withFileTypes: true }); @@ -246,7 +246,7 @@ function createMcpServer() { } try { - console.log(`Creating directory: ${dirPath}`); + console.info(`Creating directory: ${dirPath}`); await fs.mkdir(dirPath, { recursive: true }); @@ -287,7 +287,7 @@ function createMcpServer() { } try { - console.log(`Deleting file: ${filePath}`); + console.info(`Deleting file: ${filePath}`); await fs.unlink(filePath); @@ -340,7 +340,7 @@ function createMcpServer() { } try { - console.log(`Getting info for: ${filePath}`); + console.info(`Getting info for: ${filePath}`); const stats = await fs.stat(filePath); @@ -412,9 +412,9 @@ function setupSessionRoutes() { // Handle POST requests for MCP communication app.post('/mcp', async (req, res) => { try { - console.log('MCP POST request received:'); - console.log(' Headers:', JSON.stringify(req.headers, null, 2)); - console.log(' Body:', JSON.stringify(req.body, null, 2)); + console.info('MCP POST request received:'); + console.info(' Headers: %s', JSON.stringify(req.headers, null, 2)); + console.info(' Body: %s', JSON.stringify(req.body, null, 2)); // Fix missing Accept headers for compatibility with Go MCP clients // The StreamableHTTPServerTransport requires both application/json and text/event-stream @@ -424,7 +424,7 @@ function setupSessionRoutes() { !accept.includes('application/json') || !accept.includes('text/event-stream') ) { - console.log('Adding missing Accept headers for MCP compatibility'); + console.info('Adding missing Accept headers for MCP compatibility'); req.headers.accept = 'application/json, text/event-stream'; } @@ -440,7 +440,7 @@ function setupSessionRoutes() { transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (newSessionId) => { - console.log(`MCP session initialized: ${newSessionId}`); + console.info(`MCP session initialized: ${newSessionId}`); // Store the transport by session ID transports[newSessionId] = transport; }, @@ -449,7 +449,7 @@ function setupSessionRoutes() { // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { - console.log(`MCP session closed: ${transport.sessionId}`); + console.info(`MCP session closed: ${transport.sessionId}`); delete transports[transport.sessionId]; } }; @@ -528,9 +528,9 @@ Available directories: ${allowedDirectories.join(', ')} `; await fs.writeFile(sampleFile, sampleContent); - console.log(`Created sample file: ${sampleFile}`); + console.info(`Created sample file: ${sampleFile}`); } catch (error) { - console.log(`Could not create sample file in ${dir}:`, error.message); + console.info(`Could not create sample file in ${dir}:`, error.message); } } } catch (error) { @@ -552,7 +552,7 @@ app.get('/health', (req, res) => { activeSessions: Object.keys(transports).length, }; - console.log('Health check requested:', healthStatus); + console.info('Health check requested: %j', healthStatus); res.json(healthStatus); }); @@ -567,37 +567,37 @@ async function startServer() { setupSessionRoutes(); app.listen(port, host, async () => { - console.log(`MCP Filesystem server running on http://${host}:${port}`); - console.log('Protocol: Model Context Protocol (MCP)'); - console.log('Transport: Streamable HTTP'); - console.log('Available endpoints:'); - console.log(' POST /mcp - MCP protocol endpoint'); - console.log( + console.info(`MCP Filesystem server running on http://${host}:${port}`); + console.info('Protocol: Model Context Protocol (MCP)'); + console.info('Transport: Streamable HTTP'); + console.info('Available endpoints:'); + console.info(' POST /mcp - MCP protocol endpoint'); + console.info( ' GET /mcp - SSE notifications (with session-id header)' ); - console.log( + console.info( ' DELETE /mcp - Session termination (with session-id header)' ); - console.log(' GET /health - Health check'); - console.log('Available tools:'); - console.log(' - read_file - Read content from a file'); - console.log(' - write_file - Write content to a file'); - console.log(' - list_directory - List directory contents'); - console.log(' - create_directory - Create a new directory'); - console.log(' - delete_file - Delete a file'); - console.log(' - move_file - Move/rename a file'); - console.log('Allowed directories:', allowedDirectories); + console.info(' GET /health - Health check'); + console.info('Available tools:'); + console.info(' - read_file - Read content from a file'); + console.info(' - write_file - Write content to a file'); + console.info(' - list_directory - List directory contents'); + console.info(' - create_directory - Create a new directory'); + console.info(' - delete_file - Delete a file'); + console.info(' - move_file - Move/rename a file'); + console.info('Allowed directories:', allowedDirectories); // Initialize sample files await initializeSampleFiles(); - console.log('MCP Filesystem server ready for connections'); + console.info('MCP Filesystem server ready for connections'); }); } // Graceful shutdown process.on('SIGTERM', () => { - console.log('Received SIGTERM, shutting down gracefully'); + console.info('Received SIGTERM, shutting down gracefully'); // Close all transports Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); @@ -606,7 +606,7 @@ process.on('SIGTERM', () => { }); process.on('SIGINT', () => { - console.log('Received SIGINT, shutting down gracefully'); + console.info('Received SIGINT, shutting down gracefully'); // Close all transports Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); diff --git a/examples/mcp/mcp-servers/web-search/index-http.js b/examples/mcp/mcp-servers/web-search/index-http.js index 6ed6d20..513d933 100644 --- a/examples/mcp/mcp-servers/web-search/index-http.js +++ b/examples/mcp/mcp-servers/web-search/index-http.js @@ -74,7 +74,7 @@ function createServer() { }, async ({ url, timeout = 10000 }) => { try { - console.log(`Fetching URL: ${url}`); + console.info(`Fetching URL: ${url}`); const response = await axios.get(url, { timeout, headers: { @@ -151,7 +151,7 @@ function createServer() { }, }, async ({ query, limit = 5 }) => { - console.log(`Searching for: "${query}" (limit: ${limit})`); + console.info(`Searching for: "${query}" (limit: ${limit})`); const searchResults = generateSearchResults(query, limit); return { @@ -180,7 +180,7 @@ function createServer() { }, async ({ url }) => { try { - console.log(`Extracting title from: ${url}`); + console.info(`Extracting title from: ${url}`); const response = await axios.get(url, { timeout: 10000, headers: { @@ -222,8 +222,8 @@ function createServer() { */ app.post('/mcp', async (req, res) => { try { - console.log('HTTP JSON-RPC request received:'); - console.log(' Body:', JSON.stringify(req.body, null, 2)); + console.info('HTTP JSON-RPC request received:'); + console.info(' Body: %s', JSON.stringify(req.body, null, 2)); // Create new server and transport instances for each request (stateless mode) const server = createServer(); @@ -233,7 +233,7 @@ app.post('/mcp', async (req, res) => { // Clean up on request close res.on('close', () => { - console.log('Request closed'); + console.info('Request closed'); transport.close(); server.close(); }); @@ -261,7 +261,7 @@ app.post('/mcp', async (req, res) => { // Handle unsupported methods for stateless mode app.get('/mcp', async (req, res) => { - console.log('Received GET MCP request'); + console.info('Received GET MCP request'); res.status(405).json({ jsonrpc: '2.0', error: { @@ -273,7 +273,7 @@ app.get('/mcp', async (req, res) => { }); app.delete('/mcp', async (req, res) => { - console.log('Received DELETE MCP request'); + console.info('Received DELETE MCP request'); res.status(405).json({ jsonrpc: '2.0', error: { @@ -295,7 +295,7 @@ app.get('/health', (req, res) => { protocol: 'HTTP JSON-RPC', }; - console.log('Health check requested:', healthStatus); + console.info('Health check requested: %j', healthStatus); res.json(healthStatus); }); @@ -304,28 +304,28 @@ const port = process.env.PORT || 3001; const host = process.env.HOST || '0.0.0.0'; app.listen(port, host, () => { - console.log(`HTTP Web Search server running on http://${host}:${port}`); - console.log('Protocol: HTTP JSON-RPC 2.0'); - console.log('Available endpoints:'); - console.log(' POST /mcp - JSON-RPC endpoint'); - console.log(' GET /health - Health check'); - console.log('Available methods:'); - console.log(' - initialize - Initialize the server'); - console.log(' - tools/list - List available tools'); - console.log(' - tools/call - Call a tool'); - console.log('Available tools:'); - console.log(' - fetch_url - Fetch content from a URL'); - console.log(' - search_web - Perform web search (simulated)'); - console.log(' - get_page_title - Extract title from a web page'); + console.info(`HTTP Web Search server running on http://${host}:${port}`); + console.info('Protocol: HTTP JSON-RPC 2.0'); + console.info('Available endpoints:'); + console.info(' POST /mcp - JSON-RPC endpoint'); + console.info(' GET /health - Health check'); + console.info('Available methods:'); + console.info(' - initialize - Initialize the server'); + console.info(' - tools/list - List available tools'); + console.info(' - tools/call - Call a tool'); + console.info('Available tools:'); + console.info(' - fetch_url - Fetch content from a URL'); + console.info(' - search_web - Perform web search (simulated)'); + console.info(' - get_page_title - Extract title from a web page'); }); // Graceful shutdown process.on('SIGTERM', () => { - console.log('Received SIGTERM, shutting down gracefully'); + console.info('Received SIGTERM, shutting down gracefully'); process.exit(0); }); process.on('SIGINT', () => { - console.log('Received SIGINT, shutting down gracefully'); + console.info('Received SIGINT, shutting down gracefully'); process.exit(0); }); diff --git a/examples/mcp/mcp-servers/web-search/index.js b/examples/mcp/mcp-servers/web-search/index.js index 592045e..eadea5b 100644 --- a/examples/mcp/mcp-servers/web-search/index.js +++ b/examples/mcp/mcp-servers/web-search/index.js @@ -43,7 +43,7 @@ function createMcpServer() { }, async ({ url, timeout = 10000 }) => { try { - console.log(`Fetching URL: ${url}`); + console.info(`Fetching URL: ${url}`); const response = await axios.get(url, { timeout, @@ -122,7 +122,7 @@ function createMcpServer() { .describe('Maximum number of results to return'), }, async ({ query, limit = 5 }) => { - console.log(`Searching for: "${query}" (limit: ${limit})`); + console.info(`Searching for: "${query}" (limit: ${limit})`); // Generate simulated search results const searchResults = generateSearchResults(query, limit); @@ -146,7 +146,7 @@ function createMcpServer() { }, async ({ url }) => { try { - console.log(`Extracting title from: ${url}`); + console.info(`Extracting title from: ${url}`); const response = await axios.get(url, { timeout: 10000, @@ -215,9 +215,9 @@ function generateSearchResults(query, limit) { // Handle POST requests for MCP communication app.post('/mcp', async (req, res) => { try { - console.log('MCP POST request received:'); - console.log(' Headers:', JSON.stringify(req.headers, null, 2)); - console.log(' Body:', JSON.stringify(req.body, null, 2)); + console.info('MCP POST request received:'); + console.info(' Headers: %s', JSON.stringify(req.headers, null, 2)); + console.info(' Body: %s', JSON.stringify(req.body, null, 2)); // Fix missing Accept headers for compatibility with Go MCP clients // The StreamableHTTPServerTransport requires both application/json and text/event-stream @@ -227,7 +227,7 @@ app.post('/mcp', async (req, res) => { !accept.includes('application/json') || !accept.includes('text/event-stream') ) { - console.log('Adding missing Accept headers for MCP compatibility'); + console.info('Adding missing Accept headers for MCP compatibility'); req.headers.accept = 'application/json, text/event-stream'; } @@ -243,7 +243,7 @@ app.post('/mcp', async (req, res) => { transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (newSessionId) => { - console.log(`MCP session initialized: ${newSessionId}`); + console.info(`MCP session initialized: ${newSessionId}`); // Store the transport by session ID transports[newSessionId] = transport; }, @@ -252,7 +252,7 @@ app.post('/mcp', async (req, res) => { // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { - console.log(`MCP session closed: ${transport.sessionId}`); + console.info(`MCP session closed: ${transport.sessionId}`); delete transports[transport.sessionId]; } }; @@ -315,7 +315,7 @@ app.get('/health', (req, res) => { transport: 'Streamable HTTP', }; - console.log('Health check requested:', healthStatus); + console.info('Health check requested: %j', healthStatus); res.json(healthStatus); }); @@ -325,27 +325,27 @@ const port = process.env.PORT || 3001; const host = process.env.HOST || '0.0.0.0'; app.listen(port, host, () => { - console.log(`MCP Web Search server running on http://${host}:${port}`); - console.log('Protocol: Model Context Protocol (MCP)'); - console.log('Transport: Streamable HTTP'); - console.log('Available endpoints:'); - console.log(' POST /mcp - MCP protocol endpoint'); - console.log( + console.info(`MCP Web Search server running on http://${host}:${port}`); + console.info('Protocol: Model Context Protocol (MCP)'); + console.info('Transport: Streamable HTTP'); + console.info('Available endpoints:'); + console.info(' POST /mcp - MCP protocol endpoint'); + console.info( ' GET /mcp - SSE notifications (with session-id header)' ); - console.log( + console.info( ' DELETE /mcp - Session termination (with session-id header)' ); - console.log(' GET /health - Health check'); - console.log('Available tools:'); - console.log(' - fetch_url - Fetch content from a URL'); - console.log(' - search_web - Perform web search (simulated)'); - console.log(' - get_page_title - Extract title from a web page'); + console.info(' GET /health - Health check'); + console.info('Available tools:'); + console.info(' - fetch_url - Fetch content from a URL'); + console.info(' - search_web - Perform web search (simulated)'); + console.info(' - get_page_title - Extract title from a web page'); }); // Graceful shutdown process.on('SIGTERM', () => { - console.log('Received SIGTERM, shutting down gracefully'); + console.info('Received SIGTERM, shutting down gracefully'); // Close all transports Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); @@ -354,7 +354,7 @@ process.on('SIGTERM', () => { }); process.on('SIGINT', () => { - console.log('Received SIGINT, shutting down gracefully'); + console.info('Received SIGINT, shutting down gracefully'); // Close all transports Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); diff --git a/src/client.ts b/src/client.ts index 3ce770d..26f113d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -95,14 +95,29 @@ class StreamProcessor { } try { - const chunk: SchemaCreateChatCompletionStreamResponse = JSON.parse(data); - this.callbacks.onChunk?.(chunk); + const chunk: StreamChunkWithError = JSON.parse(data); + + // Handle mid-stream errors from the Inference Gateway + // When providers fail during streaming, the gateway embeds error info in the stream + if ('error' in chunk && chunk.error) { + const apiError: SchemaError = { + error: + typeof chunk.error === 'string' + ? chunk.error + : JSON.stringify(chunk.error), + }; + this.callbacks.onError?.(apiError); + return; + } - if (chunk.usage && this.callbacks.onUsageMetrics) { - this.callbacks.onUsageMetrics(chunk.usage); + const validChunk = chunk as SchemaCreateChatCompletionStreamResponse; + this.callbacks.onChunk?.(validChunk); + + if (validChunk.usage && this.callbacks.onUsageMetrics) { + this.callbacks.onUsageMetrics(validChunk.usage); } - const choice = chunk.choices?.[0]; + const choice = validChunk.choices?.[0]; if (!choice) return; this.handleReasoningContent(choice); @@ -116,8 +131,17 @@ class StreamProcessor { this.handleFinishReason(choice); } catch (parseError) { - const errorMessage = `Failed to parse SSE data: ${(parseError as Error).message}`; - globalThis.console.error(errorMessage, { data, parseError }); + let errorMessage = `Failed to parse SSE data: ${(parseError as Error).message}`; + + const errorMatch = data.match(/"error":\s*"([^"]+)"/); + if (errorMatch) { + errorMessage = errorMatch[1]; + } else { + const nestedErrorMatch = data.match(/"message":\s*"([^"]+)"/); + if (nestedErrorMatch) { + errorMessage = nestedErrorMatch[1]; + } + } const apiError: SchemaError = { error: errorMessage, @@ -520,3 +544,8 @@ export class InferenceGatewayClient { } } } + +// Add type definition for stream chunks that may contain errors +type StreamChunkWithError = SchemaCreateChatCompletionStreamResponse & { + error?: string | object; +}; From 75078d80e2681385480dcd19770f2208e37be51a Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sat, 31 May 2025 18:48:11 +0000 Subject: [PATCH 10/38] docs(examples) Add more examples and MCP server to MCP examples Signed-off-by: Eden Reich --- examples/mcp/.env.example | 52 + examples/mcp/README.md | 241 ++- examples/mcp/docker-compose.yml | 33 +- examples/mcp/example-advanced.ts | 261 +++ examples/mcp/example-basic.ts | 130 ++ examples/mcp/example-context7.ts | 198 +++ examples/mcp/example-debug-args.ts | 66 + examples/mcp/example-handler-demo.ts | 196 +++ examples/mcp/example-list-tools.ts | 29 + examples/mcp/example-nextjs.ts | 283 +++ examples/mcp/example-tool-demo.ts | 221 +++ examples/mcp/index.ts | 436 ----- examples/mcp/mcp-servers/README.md | 41 +- examples/mcp/mcp-servers/context7/README.md | 182 ++ examples/mcp/mcp-servers/context7/index.js | 696 ++++++++ .../mcp-servers/context7/package-lock.json | 1543 +++++++++++++++++ .../mcp/mcp-servers/context7/package.json | 23 + examples/mcp/package-lock.json | 294 +++- examples/mcp/package.json | 19 +- examples/mcp/tsconfig.json | 2 +- 20 files changed, 4476 insertions(+), 470 deletions(-) create mode 100644 examples/mcp/.env.example create mode 100644 examples/mcp/example-advanced.ts create mode 100644 examples/mcp/example-basic.ts create mode 100644 examples/mcp/example-context7.ts create mode 100644 examples/mcp/example-debug-args.ts create mode 100644 examples/mcp/example-handler-demo.ts create mode 100644 examples/mcp/example-list-tools.ts create mode 100644 examples/mcp/example-nextjs.ts create mode 100644 examples/mcp/example-tool-demo.ts delete mode 100644 examples/mcp/index.ts create mode 100644 examples/mcp/mcp-servers/context7/README.md create mode 100644 examples/mcp/mcp-servers/context7/index.js create mode 100644 examples/mcp/mcp-servers/context7/package-lock.json create mode 100644 examples/mcp/mcp-servers/context7/package.json diff --git a/examples/mcp/.env.example b/examples/mcp/.env.example new file mode 100644 index 0000000..e757694 --- /dev/null +++ b/examples/mcp/.env.example @@ -0,0 +1,52 @@ + +# General settings +ENVIRONMENT=development +ENABLE_TELEMETRY=false +ENABLE_AUTH=false +# Model Context Protocol (MCP) +MCP_ENABLE=false +MCP_EXPOSE=false +MCP_SERVERS= +MCP_CLIENT_TIMEOUT=5s +MCP_DIAL_TIMEOUT=3s +MCP_TLS_HANDSHAKE_TIMEOUT=3s +MCP_RESPONSE_HEADER_TIMEOUT=3s +MCP_EXPECT_CONTINUE_TIMEOUT=1s +MCP_REQUEST_TIMEOUT=5s +# OpenID Connect +OIDC_ISSUER_URL=http://keycloak:8080/realms/inference-gateway-realm +OIDC_CLIENT_ID=inference-gateway-client +OIDC_CLIENT_SECRET= +# Server settings +SERVER_HOST=0.0.0.0 +SERVER_PORT=8080 +SERVER_READ_TIMEOUT=30s +SERVER_WRITE_TIMEOUT=30s +SERVER_IDLE_TIMEOUT=120s +SERVER_TLS_CERT_PATH= +SERVER_TLS_KEY_PATH= +# Client settings +CLIENT_TIMEOUT=30s +CLIENT_MAX_IDLE_CONNS=20 +CLIENT_MAX_IDLE_CONNS_PER_HOST=20 +CLIENT_IDLE_CONN_TIMEOUT=30s +CLIENT_TLS_MIN_VERSION=TLS12 +# Providers +ANTHROPIC_API_URL=https://api.anthropic.com/v1 +ANTHROPIC_API_KEY= +CLOUDFLARE_API_URL=https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai +CLOUDFLARE_API_KEY= +COHERE_API_URL=https://api.cohere.ai +COHERE_API_KEY= +GROQ_API_URL=https://api.groq.com/openai/v1 +GROQ_API_KEY= +OLLAMA_API_URL=http://ollama:8080/v1 +OLLAMA_API_KEY= +OPENAI_API_URL=https://api.openai.com/v1 +OPENAI_API_KEY= +DEEPSEEK_API_URL=https://api.deepseek.com +DEEPSEEK_API_KEY= + +# Chosen provider +PROVIDER=deepseek +LLM=deepseek-chat diff --git a/examples/mcp/README.md b/examples/mcp/README.md index cd134b9..db93e09 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -1,8 +1,37 @@ -# MCP Example +# MCP Examples -This example demonstrates how to use the Inference Gateway SDK with Model Context Protocol (MCP) tools in a multi-provider architecture. It showcases how to connect to MCP servers, discover available tools, and use them in AI conversations. +This directory contains comprehensive examples demonstrating how to use the Inference Gateway SDK with Model Context Protocol (MCP) tools in a multi-provider architecture. Each example showcases different aspects of MCP tool integration. -Please ensure you have no containers running before starting this example, as it uses Docker Compose to set up the necessary infrastructure. +## 📁 Available Examples + +### Core Examples + +- **`example-basic.ts`** - Basic MCP tool usage and file operations +- **`example-advanced.ts`** - Multi-scenario demonstration with complex workflows +- **`example-nextjs.ts`** - Next.js application generator with documentation fetching +- **`example-handler-demo.ts`** - Comprehensive `onMCPTool` handler demonstration + +### Debugging & Testing Examples + +- **`example-tool-demo.ts`** - Tool argument analysis and issue identification +- **`example-debug-args.ts`** - Raw tool argument debugging +- **`example-list-tools.ts`** - List all available MCP tools with schemas +- **`example-mcp-tools.ts`** - Legacy MCP tools example + +## 🚀 Quick Start + +### Run Specific Examples + +```bash +# Run specific examples by name +npm run example:basic +npm run example:advanced +npm run example:nextjs +npm run example:tool-demo +npm run example:handler-demo +npm run example:debug-args +npm run example:list-tools +``` ## Features Demonstrated @@ -12,6 +41,8 @@ Please ensure you have no containers running before starting this example, as it 4. **Multi-Tool Conversations** - Combine multiple MCP tools in single conversations 5. **Tool Function Calling** - Stream responses with real-time tool execution 6. **Data Analysis** - Analyze sample data files with AI assistance +7. **Enhanced Tool Debugging** - Comprehensive `onMCPTool` handler examples +8. **Error Handling** - Robust handling of incomplete tool arguments and failures ## Architecture @@ -39,14 +70,71 @@ The `/shared` directory contains example files for testing: - `sample_sales_data.csv` - Sales data for analysis exercises - `README.md` - Documentation about available files -## Getting Started +## Setup Instructions ### Prerequisites - Docker and Docker Compose installed - API key for at least one provider (OpenAI, Groq, Anthropic, etc.) -Make sure the environment is configured: +### 1. Environment Configuration + +Copy the example environment file: + +```bash +cp .env.example .env +``` + +Configure your environment: + +```bash +# Required: Set your provider and model +PROVIDER=openai +LLM=gpt-4o + +# Required: Add your API keys (at least one) +OPENAI_API_KEY=your_key_here +GROQ_API_KEY=your_key_here +ANTHROPIC_API_KEY=your_key_here +DEEPSEEK_API_KEY=your_key_here + +# Optional: Inference Gateway configuration +EXPOSE_MCP=true +``` + +### 2. Start Infrastructure + +Start the MCP infrastructure: + +```bash +npm run compose:up +``` + +Wait for all services to be healthy: + +```bash +docker-compose ps +``` + +### 3. Install Dependencies + +```bash +npm install +``` + +### 4. Run Examples + +Choose any example to run: + +```bash +# Start with the basic example +npm run start + +# Or run specific examples +npm run run:advanced +npm run run:nextjs +npm run run:tool-demo +``` ```bash cp .env.example .env @@ -112,43 +200,153 @@ npx tsx filesystem-demo.ts ## Available Examples -- `index.ts` - Complete MCP example with multiple scenarios -- `filesystem-demo.ts` - Focused demonstration of filesystem operations -- `test-mcp-tools.ts` - Simple verification that MCP tools are working -- `advanced-example.ts` - More complex MCP usage patterns +- `index.ts` - Enhanced MCP demonstration with comprehensive tool handling +- `advanced-example.ts` - Multi-scenario examples showing file operations, web content, and data analysis +- `nextjs-example.ts` - **IMPROVED!** Create a complete Next.js app with enhanced error handling and URL parsing +- `handler-demo.ts` - Focused demonstration of the onMCPTool handler with detailed metrics and logging +- `tool-demo.ts` - **NEW!** Comprehensive tool demonstration that identifies schema issues +- `list-tools.ts` - **NEW!** Inspect MCP tool schemas and parameters +- `example-mcp-tools.ts` - Basic MCP tool discovery and testing ## Available Commands -- `npm start` - Run the main MCP example +- `npm start` - Run the main enhanced MCP example (index.ts) +- `npm run advanced` - Run the advanced multi-scenario examples +- `npm run nextjs` - Run improved Next.js app creator with better debugging +- `npm run handler-demo` - Run the focused MCP tool handler demonstration +- `npx tsx tool-demo.ts` - **NEW!** Run comprehensive tool testing and issue identification +- `npx tsx list-tools.ts` - **NEW!** List all MCP tools with detailed schemas - `npm run compose:up` - Start all services in background - `npm run compose:down` - Stop all services - `npm run compose:logs` - View logs from all services +## Key Features Demonstrated + +### Enhanced Tool Handling + +The examples now showcase: + +1. **Separate MCP and Regular Tool Handlers**: + + - `onMCPTool` - Handles tools from MCP servers + - `onTool` - Handles client-provided tools + - Enhanced logging with tool IDs and formatted arguments + +2. **Comprehensive Stream Callbacks**: + + - `onOpen` - Connection established + - `onContent` - Streaming content + - `onMCPTool` - MCP tool calls with detailed logging + - `onUsageMetrics` - Token usage tracking + - `onFinish` - Stream completion + +3. **Enhanced Error Handling**: + + - Graceful handling of malformed JSON in tool arguments + - Multiple fallback field names for URL parsing (`url`, `target_url`, `webpage_url`) + - Detailed logging of parse errors and missing parameters + +4. **Tool Argument Debugging**: + - Raw argument display for troubleshooting + - Structured argument parsing with error recovery + - Issue identification for incomplete tool schemas + +### Known Issues and Workarounds + +The examples demonstrate both working features and current limitations: + +**✅ Working:** + +- File system operations (read_file, write_file, list_directory, etc.) +- Complete argument passing with path, content, and mcpServer parameters +- Robust error handling and logging + +**❌ Current Issues:** + +- Web tools (fetch_url, search_web) receive incomplete arguments +- Missing URL parameters due to incomplete schema exposure +- LLM receives partial tool definitions from inference gateway + +**🔧 Demonstrated Solutions:** + +- Enhanced argument parsing with multiple field fallbacks +- Graceful error handling when tools fail +- Comprehensive logging to identify root causes +- Tool schema inspection utilities + +See `MCP_IMPROVEMENT_SUMMARY.md` for detailed analysis and findings. + +- `onError` - Error handling + +3. **Better Tool Call Visualization**: + - Tool call counting + - Formatted argument display + - Tool execution tracking + - Performance metrics + +## 🆕 Next.js App Creator Example + +The `nextjs-example.ts` demonstrates a powerful real-world use case: + +### What it does: + +1. **Fetches Official Documentation** - Uses MCP web tools to get the latest Next.js docs +2. **Creates Complete App Structure** - Builds a production-ready Next.js application +3. **Follows Best Practices** - Uses the fetched documentation to ensure current patterns +4. **TypeScript Setup** - Includes proper TypeScript configuration +5. **Modern Features** - Implements App Router, Server Components, and latest Next.js features + +### Features Demonstrated: + +- **Documentation-Driven Development** - AI reads official docs before coding +- **Complex File Operations** - Creates entire application structures +- **Web Content + File Operations** - Combines multiple MCP tool types +- **Production-Ready Output** - Generates runnable Next.js applications + +### Run the Example: + +```bash +npm run nextjs +``` + +The AI will: + +- Fetch Next.js documentation from https://nextjs.org/docs +- Create a complete application in `/tmp/nextjs-app/` +- Include package.json, configs, pages, components, and README +- Follow the latest Next.js best practices and conventions + ## Example Prompts to Try Once the example is running, you can ask the AI: -1. **List available data:** +1. **File Operations Chain:** ``` - "Can you show me what files are available in the /shared directory?" + "Create a JSON config file at /tmp/config.json with sample data, read it back, and list the directory" ``` -2. **Analyze sample data:** +2. **Multi-step Analysis:** ``` - "Read the sales data from /shared/sample_sales_data.csv and give me a summary of the top-selling products" + "Read the sales data from /shared/sample_sales_data.csv, analyze trends, and create a summary report" ``` -3. **Create reports:** +3. **Web Research:** ``` - "Based on the sales data, create a summary report and save it to /tmp/sales_report.txt" + "Fetch content from https://httpbin.org/json and tell me what information it contains" ``` -4. **File operations:** +4. **Documentation-Based Development:** + ``` - "Create a todo list with 5 tasks and save it to /tmp/todo.txt, then read it back to me" + "Create a React component library by first fetching React documentation, then building reusable components" + ``` + +5. **Complex File Tasks:** + ``` + "Create a todo list with 5 tasks, save it to /tmp/todo.txt, then read it back and add 2 more tasks" ``` ## Example Output @@ -264,6 +462,13 @@ Created at: Mon May 27 10:30:00 UTC 2025 - **Tools**: `fetch_url`, `search_web` - **Features**: HTTP requests, basic content extraction +### Context7 Server + +- **Purpose**: Library documentation and context resolution +- **Port**: 3002 +- **Tools**: `resolve_library_id`, `get_library_docs`, `search_libraries` +- **Features**: Library search, documentation retrieval, version management + ## Supported Providers All Inference Gateway providers work with MCP tools: diff --git a/examples/mcp/docker-compose.yml b/examples/mcp/docker-compose.yml index c3bb0c2..bc1157f 100644 --- a/examples/mcp/docker-compose.yml +++ b/examples/mcp/docker-compose.yml @@ -5,12 +5,12 @@ services: - '8080:8080' environment: # General settings - ENVIRONMENT: production + ENVIRONMENT: development # Enable MCP support MCP_ENABLE: 'true' MCP_EXPOSE: 'true' - MCP_SERVERS: 'http://mcp-filesystem:3000/mcp,http://mcp-web-search:3001/mcp' + MCP_SERVERS: 'http://mcp-filesystem:3000/mcp,http://mcp-web-search:3001/mcp,http://mcp-context7:3002/mcp' # Server settings SERVER_HOST: '0.0.0.0' @@ -37,6 +37,8 @@ services: condition: service_healthy mcp-web-search: condition: service_healthy + mcp-context7: + condition: service_healthy networks: - inference-network healthcheck: @@ -103,6 +105,33 @@ services: start_period: 45s restart: unless-stopped + mcp-context7: + build: + context: ./mcp-servers/context7 + dockerfile_inline: | + FROM node:18-alpine + WORKDIR /app + RUN apk add --no-cache curl + COPY package.json ./ + RUN npm install + COPY . . + EXPOSE 3002 + CMD ["npm", "start"] + environment: + NODE_ENV: 'production' + MCP_SERVER_NAME: 'context7' + MCP_SERVER_VERSION: '1.0.0' + PORT: '3002' + networks: + - inference-network + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:3002/health'] + interval: 30s + timeout: 10s + retries: 5 + start_period: 45s + restart: unless-stopped + # # Optional: Ollama for local models # ollama: # image: ollama/ollama:latest diff --git a/examples/mcp/example-advanced.ts b/examples/mcp/example-advanced.ts new file mode 100644 index 0000000..129ea7d --- /dev/null +++ b/examples/mcp/example-advanced.ts @@ -0,0 +1,261 @@ +import { + InferenceGatewayClient, + MessageRole, + Provider, +} from '../../src/index.js'; + +(async () => { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + const provider = (process.env.PROVIDER as Provider) || Provider.groq; + const model = process.env.LLM || 'llama-3.3-70b-versatile'; + + console.info(`🚀 Advanced MCP Demo using ${model} on ${provider}\n`); + + try { + // Health check + const isHealthy = await client.healthCheck(); + if (!isHealthy) { + console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); + process.exit(1); + } + + // Discover available MCP tools + const tools = await client.listTools(); + console.info(`📋 Found ${tools.data.length} MCP tools available:`); + tools.data.forEach((tool, index) => { + console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); + }); + console.info(''); + + if (tools.data.length === 0) { + console.error( + '⚠️ No MCP tools available. Check MCP server configuration.' + ); + return; + } + + // Example 1: File Operations Chain + console.info('=== Example 1: File Operations Chain ===\n'); + await runFileOperationsExample(client, model, provider); + + console.info('\n' + '='.repeat(50) + '\n'); + + // Example 2: Web Content Analysis + console.info('=== Example 2: Web Content Analysis ===\n'); + await runWebContentExample(client, model, provider); + + console.info('\n' + '='.repeat(50) + '\n'); + + // Example 3: Data Analysis with CSV + console.info('=== Example 3: Data Analysis ===\n'); + await runDataAnalysisExample(client, model, provider); + } catch (error) { + if ( + error instanceof Error && + error.message.includes('MCP tools endpoint is not exposed') + ) { + console.error( + '❌ MCP not exposed. Set EXPOSE_MCP=true and restart gateway.' + ); + } else { + console.error('❌ Error:', error); + } + } +})(); + +async function runFileOperationsExample( + client: InferenceGatewayClient, + model: string, + provider: Provider +) { + let toolCallCount = 0; + let contentBuffer = ''; + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: `You are a file management assistant. Use the available MCP tools to: +1. Create a configuration file at /tmp/config.json with sample data +2. Read the file back to verify +3. List the /tmp directory to show what's there +4. Provide a summary of what you accomplished + +Be detailed about each step and confirm successful operations.`, + }, + { + role: MessageRole.user, + content: + 'Help me set up a sample configuration file with some JSON data.', + }, + ], + max_tokens: 1000, + }, + { + onContent: (content) => { + process.stdout.write(content); + contentBuffer += content; + }, + onMCPTool: (toolCall) => { + toolCallCount++; + console.info( + `\n🛠️ [${toolCallCount}] MCP Tool: ${toolCall.function.name}` + ); + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); + } catch { + console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); + } + console.info(`🔍 Tool ID: ${toolCall.id}`); + console.info(''); // Add spacing before next content + }, + onUsageMetrics: (usage) => { + console.info( + `\n📊 Tokens - Prompt: ${usage.prompt_tokens}, Completion: ${usage.completion_tokens}, Total: ${usage.total_tokens}` + ); + }, + onFinish: () => { + console.info( + `\n✅ File operations completed! Used ${toolCallCount} MCP tools.\n` + ); + }, + onError: (error) => { + console.error('\n❌ Stream Error:', error); + }, + }, + provider + ); +} + +async function runWebContentExample( + client: InferenceGatewayClient, + model: string, + provider: Provider +) { + let toolCallCount = 0; + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: `You are a web research assistant. Use the fetch_url tool to get content from URLs and analyze it. +Provide summaries and key insights from the content you retrieve.`, + }, + { + role: MessageRole.user, + content: + 'Can you fetch the content from https://httpbin.org/json and tell me what information it contains?', + }, + ], + max_tokens: 800, + }, + { + onContent: (content) => { + process.stdout.write(content); + }, + onMCPTool: (toolCall) => { + toolCallCount++; + console.info( + `\n🌐 [${toolCallCount}] Web Tool: ${toolCall.function.name}` + ); + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`🔗 URL: ${args.url || 'N/A'}`); + if (args.timeout) { + console.info(`⏱️ Timeout: ${args.timeout}ms`); + } + } catch { + console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); + } + console.info(`🔍 Tool ID: ${toolCall.id}`); + console.info(''); // Add spacing + }, + onFinish: () => { + console.info( + `\n✅ Web content analysis completed! Used ${toolCallCount} web tools.\n` + ); + }, + onError: (error) => { + console.error('\n❌ Stream Error:', error); + }, + }, + provider + ); +} + +async function runDataAnalysisExample( + client: InferenceGatewayClient, + model: string, + provider: Provider +) { + let toolCallCount = 0; + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: `You are a data analyst. Use the available MCP tools to: +1. Read the CSV file at /shared/sample_sales_data.csv +2. Analyze the data structure and content +3. Provide insights about the data +4. Create a summary report and save it to /tmp/analysis_report.txt + +Be thorough in your analysis and explanations.`, + }, + { + role: MessageRole.user, + content: + 'Please analyze the sample sales data and create a comprehensive report.', + }, + ], + max_tokens: 1200, + }, + { + onContent: (content) => { + process.stdout.write(content); + }, + onMCPTool: (toolCall) => { + toolCallCount++; + console.info( + `\n📊 [${toolCallCount}] Analysis Tool: ${toolCall.function.name}` + ); + try { + const args = JSON.parse(toolCall.function.arguments); + if (args.file_path) { + console.info(`📁 File: ${args.file_path}`); + } + if (args.content && args.content.length > 100) { + console.info( + `📝 Content: ${args.content.substring(0, 100)}... (${args.content.length} chars)` + ); + } else if (args.content) { + console.info(`📝 Content: ${args.content}`); + } + } catch { + console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); + } + console.info(`🔍 Tool ID: ${toolCall.id}`); + console.info(''); // Add spacing + }, + onFinish: () => { + console.info( + `\n✅ Data analysis completed! Used ${toolCallCount} analysis tools.\n` + ); + }, + onError: (error) => { + console.error('\n❌ Stream Error:', error); + }, + }, + provider + ); +} diff --git a/examples/mcp/example-basic.ts b/examples/mcp/example-basic.ts new file mode 100644 index 0000000..adf3d82 --- /dev/null +++ b/examples/mcp/example-basic.ts @@ -0,0 +1,130 @@ +import { + InferenceGatewayClient, + MessageRole, + Provider, +} from '../../src/index.js'; + +(async () => { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + const provider = (process.env.PROVIDER as Provider) || Provider.groq; + const model = process.env.LLM || 'llama-3.3-70b-versatile'; + + console.info(`🚀 Using ${model} on ${provider}\n`); + + try { + // Health check + const isHealthy = await client.healthCheck(); + if (!isHealthy) { + console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); + process.exit(1); + } + + // Discover available MCP tools + const tools = await client.listTools(); + console.info(`📋 Found ${tools.data.length} MCP tools available\n`); + + if (tools.data.length === 0) { + console.error( + '⚠️ No MCP tools available. Check MCP server configuration.' + ); + return; + } + + // Display available tools + console.info('📋 Available MCP Tools:'); + tools.data.forEach((tool, index) => { + console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); + }); + console.info(''); + + // Comprehensive MCP tool demonstration + console.info('=== MCP Tool Demo ===\n'); + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: `You are a helpful assistant with access to MCP tools for file operations and web content fetching. + +IMPORTANT: You MUST use the available tools to complete tasks. When asked to work with files: +1. Use write_file to create files +2. Use read_file to read content +3. Use list_directory to explore directories +4. Always confirm what you did + +For web content: +1. Use fetch_url to get content from URLs +2. Summarize and analyze the content + +Be verbose about your tool usage and explain what you're doing step by step.`, + }, + { + role: MessageRole.user, + content: + 'Please write "Hello MCP Tools!" to /tmp/demo.txt, then read it back to confirm, and finally list the /tmp directory contents.', + }, + ], + max_tokens: 800, + }, + { + onOpen: () => { + console.info('🔗 Connection opened, starting stream...\n'); + }, + onContent: (content) => { + process.stdout.write(content); + }, + onMCPTool: (toolCall) => { + console.info(`\n🛠️ MCP Tool Called: ${toolCall.function.name}`); + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); + } catch { + console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); + } + console.info(`🔍 Tool ID: ${toolCall.id}\n`); + }, + onTool: (toolCall) => { + // This would handle regular (non-MCP) tools if any were provided + console.info(`\n🔧 Regular Tool Called: ${toolCall.function.name}`); + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); + } catch { + console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); + } + console.info(`🔍 Tool ID: ${toolCall.id}\n`); + }, + onUsageMetrics: (usage) => { + console.info(`\n📊 Token Usage:`, { + prompt: usage.prompt_tokens, + completion: usage.completion_tokens, + total: usage.total_tokens, + }); + }, + onFinish: () => { + console.info('\n\n✅ Demo completed successfully!\n'); + }, + onError: (error) => { + console.error('\n❌ Stream Error:', error); + }, + }, + provider + ); + } catch (error) { + if ( + error instanceof Error && + error.message.includes('MCP tools endpoint is not exposed') + ) { + console.error( + '❌ MCP not exposed. Set EXPOSE_MCP=true and restart gateway.' + ); + } else { + console.error('❌ Error:', error); + } + } +})(); diff --git a/examples/mcp/example-context7.ts b/examples/mcp/example-context7.ts new file mode 100644 index 0000000..5433baf --- /dev/null +++ b/examples/mcp/example-context7.ts @@ -0,0 +1,198 @@ +/** + * Context7 MCP Server Usage Example + * + * This example demonstrates how to use the Context7 MCP server + * to resolve library IDs and fetch documentation through the LLM. + */ + +import * as dotenv from 'dotenv'; +import { + InferenceGatewayClient, + MessageRole, + Provider, +} from '../../src/index.js'; + +dotenv.config(); + +// For ES modules compatibility +declare const require: any; +declare const module: any; + +async function demonstrateContext7() { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + const provider = (process.env.PROVIDER as Provider) || Provider.groq; + const model = process.env.LLM || 'llama-3.3-70b-versatile'; + + console.log(`🚀 Context7 MCP Server Demo using ${model} on ${provider}\n`); + + try { + // Health check + const isHealthy = await client.healthCheck(); + if (!isHealthy) { + console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); + process.exit(1); + } + + // Check if Context7 tools are available + const tools = await client.listTools(); + const context7Tools = tools.data.filter((tool) => + ['resolve_library_id', 'get_library_docs', 'search_libraries'].includes( + tool.name + ) + ); + + if (context7Tools.length === 0) { + console.error( + '⚠️ Context7 MCP tools not available. Make sure the Context7 server is running on port 3002.' + ); + return; + } + + console.info(`📋 Found ${context7Tools.length} Context7 tools available:`); + context7Tools.forEach((tool, index) => { + console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); + }); + console.info(''); + + // Demonstrate Context7 usage through LLM conversation + console.info('=== Context7 Demo: Library Research Session ===\n'); + + await client.streamChatCompletion( + { + model: `${provider + '/' + model}`, + messages: [ + { + role: MessageRole.system, + content: `You are a helpful software development assistant with access to Context7 MCP tools for library documentation and research. + +IMPORTANT: You MUST use context7 tools to complete tasks. Always use context7 for up-to-date library information: + +1. Use resolve_library_id to find the correct library ID for any library mentioned +2. Use get_library_docs to fetch detailed documentation and code examples +3. Use search_libraries to discover relevant libraries + +When helping with development tasks: +- Always resolve library IDs first using resolve_library_id +- Fetch comprehensive documentation using get_library_docs +- Provide up-to-date code examples and best practices +- Explain concepts clearly with context from the documentation + +Be thorough and always use context7 tools to provide the most current information.`, + }, + { + role: MessageRole.user, + content: `I'm starting a new React project and want to use Next.js. Can you help me by: + +1. First, resolve the library IDs for React and Next.js +2. Get the latest documentation for Next.js focusing on the App Router +3. Search for any other relevant frontend libraries I should consider +4. Provide me with a basic setup example based on the latest documentation + +Please use context7 to get the most up-to-date information.`, + }, + ], + max_tokens: 1500, + }, + { + onOpen: () => { + console.info( + '🔗 Connection opened, starting Context7 research session...\n' + ); + }, + onReasoning: (reasoning) => { + console.info(`\n🤔 Context7 Reasoning: ${reasoning}`); + }, + onContent: (content) => { + process.stdout.write(content); + }, + onMCPTool: (toolCall) => { + console.info(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); + } catch { + console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); + } + console.info(`🔍 Tool ID: ${toolCall.id}\n`); + }, + onError: (error) => { + console.error(`\n❌ Stream Error: ${error.error}`); + }, + onFinish: () => { + console.info('\n\n✅ Context7 research session completed!\n'); + }, + } + ); + + // Second example: TypeScript documentation lookup + console.info('\n=== Context7 Demo: TypeScript Documentation Lookup ===\n'); + + await client.streamChatCompletion( + { + model: `${provider + '/' + model}`, + messages: [ + { + role: MessageRole.system, + content: `You are a TypeScript expert with access to Context7 MCP tools. Always use context7 to get the latest TypeScript documentation and provide accurate, up-to-date information.`, + }, + { + role: MessageRole.user, + content: `I need help with TypeScript generics and utility types. Please use context7 to get the latest documentation on TypeScript and show me examples of: + +1. Generic functions +2. Utility types like Pick, Omit, and Partial +3. Advanced type patterns + +Use context7 to ensure you have the most current information.`, + }, + ], + max_tokens: 1000, + }, + { + onOpen: () => { + console.info('🔗 Starting TypeScript documentation lookup...\n'); + }, + onContent: (content) => { + process.stdout.write(content); + }, + onMCPTool: (toolCall) => { + console.info( + `\n📚 Context7 Documentation Tool: ${toolCall.function.name}` + ); + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`📋 Query:`, JSON.stringify(args, null, 2)); + } catch { + console.info(`📋 Raw Query: ${toolCall.function.arguments}`); + } + console.info(`🆔 Request ID: ${toolCall.id}\n`); + }, + onError: (error) => { + console.error(`\n❌ Documentation Error: ${error.error}`); + }, + onFinish: () => { + console.info('\n\n✅ TypeScript documentation session completed!\n'); + }, + } + ); + } catch (error) { + console.error('❌ Error:', (error as Error).message); + console.log( + '\n💡 Make sure the Context7 MCP server is running on port 3002' + ); + console.log(' and the Inference Gateway is running on port 8080'); + } +} + +// Run the demo +if ( + require.main === module || + process.argv[1].endsWith('example-context7.ts') +) { + demonstrateContext7().catch(console.error); +} + +export { demonstrateContext7 }; diff --git a/examples/mcp/example-debug-args.ts b/examples/mcp/example-debug-args.ts new file mode 100644 index 0000000..e82c957 --- /dev/null +++ b/examples/mcp/example-debug-args.ts @@ -0,0 +1,66 @@ +import * as dotenv from 'dotenv'; +import { + InferenceGatewayClient, + MessageRole, + Provider, +} from '../../src/index.js'; + +dotenv.config(); + +(async () => { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + const provider = (process.env.PROVIDER as Provider) || Provider.openai; + const model = process.env.LLM || 'gpt-4o'; + + console.info(`🧪 Testing MCP Tool Arguments - ${model} on ${provider}\n`); + + try { + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.user, + content: + 'Please fetch the content from https://nextjs.org/docs and show me what you find.', + }, + ], + max_tokens: 500, + }, + { + onMCPTool: (toolCall) => { + console.info(`\n🔧 MCP Tool Called: ${toolCall.function.name}`); + console.info(`🆔 Tool ID: ${toolCall.id}`); + console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); + + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`✅ Parsed Arguments:`, JSON.stringify(args, null, 2)); + + // Show all available properties + const keys = Object.keys(args); + console.info(`🔑 Available properties: ${keys.join(', ')}`); + } catch (e) { + console.info(`❌ JSON Parse Error: ${e.message}`); + } + console.info(''); // Add spacing + }, + onContent: (content) => { + process.stdout.write(content); + }, + onFinish: () => { + console.info(`\n\n✅ Test completed!\n`); + }, + onError: (error) => { + console.error('\n❌ Error:', error); + }, + }, + provider + ); + } catch (error) { + console.error('❌ Error:', error); + } +})(); diff --git a/examples/mcp/example-handler-demo.ts b/examples/mcp/example-handler-demo.ts new file mode 100644 index 0000000..1e0f50c --- /dev/null +++ b/examples/mcp/example-handler-demo.ts @@ -0,0 +1,196 @@ +import { + InferenceGatewayClient, + MessageRole, + Provider, +} from '../../src/index.js'; + +/** + * This example specifically demonstrates the enhanced MCP tool handler features + * including the onMCPTool callback with detailed logging and tracking. + */ +(async () => { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + const provider = (process.env.PROVIDER as Provider) || Provider.groq; + const model = process.env.LLM || 'llama-3.3-70b-versatile'; + + console.info(`🎯 MCP Tool Handler Demo using ${model} on ${provider}\n`); + + try { + // Health check + const isHealthy = await client.healthCheck(); + if (!isHealthy) { + console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); + process.exit(1); + } + + // Track various metrics + let totalToolCalls = 0; + let mcpToolCalls = 0; + let regularToolCalls = 0; + const toolCallTimestamps: Array<{ + tool: string; + timestamp: Date; + id: string; + }> = []; + + console.info('=== Enhanced MCP Tool Handler Demo ===\n'); + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: `You are a helpful assistant demonstrating MCP tool usage. + +TASK: Perform the following operations step by step: +1. Create a demo file at /tmp/handler-test.txt with "MCP Handler Demo" content +2. Read the file back to verify +3. List the /tmp directory +4. Create a simple JSON file at /tmp/demo-config.json with some configuration data +5. Read that JSON file back + +Be explicit about each step and confirm completion.`, + }, + { + role: MessageRole.user, + content: + 'Please demonstrate MCP tool usage by performing the file operations listed in the system prompt.', + }, + ], + max_tokens: 1000, + }, + { + onOpen: () => { + console.info('🔌 Stream connection opened\n'); + }, + onContent: (content) => { + // Use a subtle indicator for content + process.stdout.write(content); + }, + onMCPTool: (toolCall) => { + mcpToolCalls++; + totalToolCalls++; + const timestamp = new Date(); + toolCallTimestamps.push({ + tool: toolCall.function.name, + timestamp, + id: toolCall.id, + }); + + console.info( + `\n🛠️ MCP Tool #${mcpToolCalls}: ${toolCall.function.name}` + ); + console.info(`🆔 Tool ID: ${toolCall.id}`); + console.info(`⏰ Called at: ${timestamp.toISOString()}`); + + try { + const args = JSON.parse(toolCall.function.arguments); + + // Format arguments nicely based on tool type + if (toolCall.function.name === 'write_file') { + console.info(`📁 File: ${args.path}`); + if (args.content && args.content.length > 50) { + console.info( + `📝 Content: ${args.content.substring(0, 50)}... (${args.content.length} chars)` + ); + } else { + console.info(`📝 Content: ${args.content}`); + } + console.info(`🖥️ Server: ${args.mcpServer}`); + } else if (toolCall.function.name === 'read_file') { + console.info(`📖 Reading: ${args.path}`); + console.info(`🖥️ Server: ${args.mcpServer}`); + } else if (toolCall.function.name === 'list_directory') { + console.info(`📂 Listing: ${args.path}`); + console.info(`🖥️ Server: ${args.mcpServer}`); + } else { + console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); + } + } catch (parseError) { + console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); + console.warn(`⚠️ Failed to parse arguments: ${parseError}`); + } + + console.info(''); // Add spacing for readability + }, + onTool: (toolCall) => { + // This handles regular (non-MCP) tools + regularToolCalls++; + totalToolCalls++; + console.info(`\n🔧 Regular Tool Called: ${toolCall.function.name}`); + console.info(`🆔 Tool ID: ${toolCall.id}`); + console.info(`📝 Arguments: ${toolCall.function.arguments}\n`); + }, + onUsageMetrics: (usage) => { + console.info(`\n📊 Token Metrics:`); + console.info(` Prompt Tokens: ${usage.prompt_tokens}`); + console.info(` Completion Tokens: ${usage.completion_tokens}`); + console.info(` Total Tokens: ${usage.total_tokens}`); + if (usage.prompt_tokens && usage.completion_tokens) { + const efficiency = ( + (usage.completion_tokens / usage.prompt_tokens) * + 100 + ).toFixed(1); + console.info( + ` Efficiency: ${efficiency}% (completion/prompt ratio)` + ); + } + }, + onFinish: () => { + console.info('\n' + '='.repeat(60)); + console.info('📈 Session Summary:'); + console.info(` Total Tool Calls: ${totalToolCalls}`); + console.info(` MCP Tool Calls: ${mcpToolCalls}`); + console.info(` Regular Tool Calls: ${regularToolCalls}`); + + if (toolCallTimestamps.length > 0) { + console.info('\n🕒 Tool Call Timeline:'); + toolCallTimestamps.forEach((call, index) => { + const timeStr = call.timestamp.toLocaleTimeString(); + console.info( + ` ${index + 1}. ${timeStr} - ${call.tool} (${call.id.substring(0, 8)}...)` + ); + }); + + // Calculate duration between first and last tool call + if (toolCallTimestamps.length > 1) { + const duration = + toolCallTimestamps[ + toolCallTimestamps.length - 1 + ].timestamp.getTime() - + toolCallTimestamps[0].timestamp.getTime(); + console.info(`\n⏱️ Total tool execution span: ${duration}ms`); + } + } + + console.info('\n✅ MCP Tool Handler Demo completed successfully!'); + console.info('='.repeat(60) + '\n'); + }, + onError: (error) => { + console.error('\n❌ Stream Error occurred:'); + console.error(' Error:', error); + console.error(` At: ${new Date().toISOString()}`); + console.error( + ` Total tools called before error: ${totalToolCalls}\n` + ); + }, + }, + provider + ); + } catch (error) { + if ( + error instanceof Error && + error.message.includes('MCP tools endpoint is not exposed') + ) { + console.error( + '❌ MCP not exposed. Set EXPOSE_MCP=true and restart gateway.' + ); + } else { + console.error('❌ Unexpected error:', error); + } + } +})(); diff --git a/examples/mcp/example-list-tools.ts b/examples/mcp/example-list-tools.ts new file mode 100644 index 0000000..2f7ffce --- /dev/null +++ b/examples/mcp/example-list-tools.ts @@ -0,0 +1,29 @@ +import * as dotenv from 'dotenv'; +import { InferenceGatewayClient } from '../../src/index.js'; + +dotenv.config(); + +(async () => { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + try { + const tools = await client.listTools(); + console.info(`📋 Found ${tools.data.length} MCP tools:\n`); + + tools.data.forEach((tool, index) => { + console.info(`${index + 1}. ${tool.name}`); + console.info(` Description: ${tool.description}`); + if (tool.input_schema) { + console.info( + ` Input Schema:`, + JSON.stringify(tool.input_schema, null, 2) + ); + } + console.info(''); + }); + } catch (error) { + console.error('❌ Error:', error); + } +})(); diff --git a/examples/mcp/example-nextjs.ts b/examples/mcp/example-nextjs.ts new file mode 100644 index 0000000..92fe0e9 --- /dev/null +++ b/examples/mcp/example-nextjs.ts @@ -0,0 +1,283 @@ +import * as dotenv from 'dotenv'; +import { + InferenceGatewayClient, + MessageRole, + Provider, +} from '../../src/index.js'; + +// Load environment variables from .env file +dotenv.config(); + +(async () => { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + const provider = (process.env.PROVIDER as Provider) || Provider.openai; + const model = process.env.LLM || 'gpt-4o'; + + console.info(`🚀 Next.js App Creator using ${model} on ${provider}\n`); + + try { + // Health check + const isHealthy = await client.healthCheck(); + if (!isHealthy) { + console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); + process.exit(1); + } + + // Discover available MCP tools + const tools = await client.listTools(); + console.info(`📋 Found ${tools.data.length} MCP tools available\n`); + + if (tools.data.length === 0) { + console.error( + '⚠️ No MCP tools available. Check MCP server configuration.' + ); + return; + } + + console.info('=== Next.js App Creator with Documentation ===\n'); + + let toolCallCount = 0; + let webToolCalls = 0; + let fileToolCalls = 0; + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: `You are an expert Next.js developer assistant with access to MCP tools for web content fetching and file operations. + +Your task is to: +1. Fetch the latest Next.js documentation from official sources +2. Create a complete Next.js application structure following current best practices +3. Use the documentation to ensure you're following the latest patterns and conventions +4. Create all necessary files with proper TypeScript setup +5. Include modern Next.js features like App Router, Server Components, etc. + +Available MCP tools: +- fetch_url: Get content from Next.js documentation URLs +- write_file: Create application files +- read_file: Read existing files +- list_directory: Check directory structure + +Please be thorough and create a production-ready Next.js app structure with: +- package.json with latest dependencies +- next.config.js with proper configuration +- tsconfig.json for TypeScript +- App Router structure (app/ directory) +- A sample page with components +- Basic styling setup +- README with instructions + +Always reference the official documentation to ensure accuracy.`, + }, + { + role: MessageRole.user, + content: `Please create a complete Next.js application following the latest documentation and best practices. + +First, fetch the current Next.js documentation from https://nextjs.org/docs to understand the latest features and setup requirements, then create a full application structure in the /tmp/nextjs-app/ directory. + +The app should include: +1. Modern App Router setup +2. TypeScript configuration +3. A homepage with navigation +4. A sample about page +5. Basic component structure +6. Proper styling setup (CSS modules or Tailwind) +7. Package.json with all necessary dependencies + +Make sure to follow the official documentation patterns exactly.`, + }, + ], + max_tokens: 4000, + }, + { + onOpen: () => { + console.info( + '🔗 Connection opened, starting Next.js app creation...\n' + ); + }, + onContent: (content) => { + process.stdout.write(content); + }, + onMCPTool: (toolCall) => { + toolCallCount++; + + if (toolCall.function.name === 'fetch_url') { + webToolCalls++; + console.info( + `\n🌐 [${toolCallCount}] Fetching Documentation: ${toolCall.function.name}` + ); + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`📝 Raw Arguments:`, JSON.stringify(args, null, 2)); + + // Handle different possible argument field names + const url = + args.url || + args.target_url || + args.webpage_url || + args.uri || + args.link || + (args.arguments && args.arguments.url) || + 'URL not found'; + console.info(`🔗 URL: ${url}`); + if (args.timeout) { + console.info(`⏱️ Timeout: ${args.timeout}ms`); + } + if (args.mcpServer) { + console.info(`🖥️ MCP Server: ${args.mcpServer}`); + } + } catch (e) { + console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); + console.info(`⚠️ Parse Error: ${e.message}`); + } + } else if ( + toolCall.function.name.includes('file') || + toolCall.function.name.includes('directory') + ) { + fileToolCalls++; + console.info( + `\n📁 [${toolCallCount}] File Operation: ${toolCall.function.name}` + ); + try { + const args = JSON.parse(toolCall.function.arguments); + // Handle different path field names + const path = + args.file_path || + args.path || + args.directory_path || + args.target_path; + if (path) { + const fileName = path.split('/').pop(); + console.info(`📄 Path: ${fileName} (${path})`); + } + if (args.content && args.content.length > 150) { + console.info( + `📝 Content: ${args.content.substring(0, 150)}... (${ + args.content.length + } chars)` + ); + } else if (args.content) { + console.info(`📝 Content: ${args.content}`); + } + if (args.mcpServer) { + console.info(`🖥️ MCP Server: ${args.mcpServer}`); + } + } catch (e) { + console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); + console.info(`⚠️ Parse Error: ${e.message}`); + } + } else { + console.info( + `\n🛠️ [${toolCallCount}] MCP Tool: ${toolCall.function.name}` + ); + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); + } catch (e) { + console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); + console.info(`⚠️ Parse Error: ${e.message}`); + } + } + + console.info(`🔍 Tool ID: ${toolCall.id}`); + console.info(''); // Add spacing + }, + onTool: (toolCall) => { + // Handle any regular (non-MCP) tools if present + console.info(`\n🔧 Regular Tool: ${toolCall.function.name}`); + console.info(`🔍 Tool ID: ${toolCall.id}\n`); + }, + onUsageMetrics: (usage) => { + console.info( + `\n📊 Token Usage - Prompt: ${usage.prompt_tokens}, Completion: ${usage.completion_tokens}, Total: ${usage.total_tokens}` + ); + }, + onFinish: () => { + console.info(`\n\n✅ Next.js App Creation Completed!`); + console.info(`📈 Total Tools Used: ${toolCallCount}`); + console.info(`🌐 Documentation Fetches: ${webToolCalls}`); + console.info(`📁 File Operations: ${fileToolCalls}`); + console.info( + `\n🎯 Your Next.js app has been created in /tmp/nextjs-app/` + ); + console.info(`📖 Check the README.md file for setup instructions\n`); + }, + onError: (error) => { + console.error('\n❌ Stream Error:', error); + }, + }, + provider + ); + + // After completion, show the created structure + console.info('\n=== Created File Structure ===\n'); + await showDirectoryStructure(client, model, provider); + } catch (error) { + if ( + error instanceof Error && + error.message.includes('MCP tools endpoint is not exposed') + ) { + console.error( + '❌ MCP not exposed. Set EXPOSE_MCP=true and restart gateway.' + ); + } else { + console.error('❌ Error:', error); + } + } +})(); + +async function showDirectoryStructure( + client: InferenceGatewayClient, + model: string, + provider: Provider +) { + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.system, + content: `You are a file system assistant. Use the list_directory tool to show the structure of the created Next.js application.`, + }, + { + role: MessageRole.user, + content: + 'Please show me the complete directory structure of /tmp/nextjs-app/ including all subdirectories and files.', + }, + ], + max_tokens: 1000, + }, + { + onContent: (content) => { + process.stdout.write(content); + }, + onMCPTool: (toolCall) => { + if (toolCall.function.name === 'list_directory') { + try { + const args = JSON.parse(toolCall.function.arguments); + console.info( + `\n📂 Listing: ${args.file_path || args.directory_path || 'directory'}` + ); + } catch { + console.info(`\n📂 Listing directory...`); + } + } + }, + onFinish: () => { + console.info(`\n\n🎉 Next.js application structure complete!`); + console.info(`\nTo run your app:`); + console.info(`1. cd /tmp/nextjs-app`); + console.info(`2. npm install`); + console.info(`3. npm run dev`); + console.info(`4. Open http://localhost:3000\n`); + }, + }, + provider + ); +} diff --git a/examples/mcp/example-tool-demo.ts b/examples/mcp/example-tool-demo.ts new file mode 100644 index 0000000..6c214d7 --- /dev/null +++ b/examples/mcp/example-tool-demo.ts @@ -0,0 +1,221 @@ +import * as dotenv from 'dotenv'; +import { + InferenceGatewayClient, + MessageRole, + Provider, +} from '../../src/index.js'; + +dotenv.config(); + +(async () => { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + const provider = (process.env.PROVIDER as Provider) || Provider.openai; + const model = process.env.LLM || 'gpt-4o'; + + console.info(`🔍 MCP Tool Demonstration - ${model} on ${provider}\n`); + console.info('='.repeat(60)); + console.info('🎯 PURPOSE: Demonstrate onMCPTool handler and identify issues'); + console.info('='.repeat(60)); + + try { + // Health check + const isHealthy = await client.healthCheck(); + if (!isHealthy) { + console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); + process.exit(1); + } + + // List available tools + const tools = await client.listTools(); + console.info(`\n📋 Available MCP Tools: ${tools.data.length}`); + tools.data.forEach((tool, index) => { + console.info( + ` ${index + 1}. ${tool.name} - ${tool.description.substring(0, 80)}...` + ); + }); + + console.info('\n' + '='.repeat(60)); + console.info('🧪 TEST 1: File Operations (Expected to work)'); + console.info('='.repeat(60)); + + let toolCallCount = 0; + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.user, + content: + 'Please create a simple test file at /tmp/test-demo.txt with the content "Hello MCP Demo!" and then read it back.', + }, + ], + max_tokens: 500, + }, + { + onContent: (content) => { + process.stdout.write(content); + }, + onMCPTool: (toolCall) => { + toolCallCount++; + console.info( + `\n📋 [${toolCallCount}] MCP Tool: ${toolCall.function.name}` + ); + console.info(`🆔 ID: ${toolCall.id}`); + console.info(`📝 Raw Args: ${toolCall.function.arguments}`); + + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`✅ Parsed Args:`, JSON.stringify(args, null, 2)); + + // Analyze arguments + if (args.path) { + console.info(`✅ ✓ Path parameter found: ${args.path}`); + } + if (args.content) { + console.info( + `✅ ✓ Content parameter found: ${args.content.substring(0, 50)}...` + ); + } + if (args.mcpServer) { + console.info(`✅ ✓ MCP Server specified: ${args.mcpServer}`); + } + } catch (e) { + console.info(`❌ Parse Error: ${e.message}`); + } + console.info(''); // spacing + }, + onFinish: () => { + console.info( + `\n✅ Test 1 Complete - File operations used ${toolCallCount} tools\n` + ); + runWebTest(); + }, + onError: (error) => { + console.error('\n❌ Test 1 Error:', error); + runWebTest(); + }, + }, + provider + ); + + async function runWebTest() { + console.info('='.repeat(60)); + console.info('🧪 TEST 2: Web Operations (Expected to have issues)'); + console.info('='.repeat(60)); + + toolCallCount = 0; + + await client.streamChatCompletion( + { + model, + messages: [ + { + role: MessageRole.user, + content: + 'Please fetch the content from https://example.com and show me what you find.', + }, + ], + max_tokens: 500, + }, + { + onContent: (content) => { + process.stdout.write(content); + }, + onMCPTool: (toolCall) => { + toolCallCount++; + console.info( + `\n📋 [${toolCallCount}] MCP Tool: ${toolCall.function.name}` + ); + console.info(`🆔 ID: ${toolCall.id}`); + console.info(`📝 Raw Args: ${toolCall.function.arguments}`); + + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`✅ Parsed Args:`, JSON.stringify(args, null, 2)); + + // Analyze web tool arguments + const urlFields = [ + 'url', + 'target_url', + 'webpage_url', + 'uri', + 'link', + ]; + const foundUrl = urlFields.find((field) => args[field]); + + if (foundUrl) { + console.info(`✅ ✓ URL parameter found: ${args[foundUrl]}`); + } else { + console.info(`❌ ✗ No URL parameter found`); + console.info( + `❌ ✗ Available fields: ${Object.keys(args).join(', ')}` + ); + console.info(`❌ ✗ Expected fields: ${urlFields.join(', ')}`); + } + + if (args.mcpServer) { + console.info(`✅ ✓ MCP Server specified: ${args.mcpServer}`); + } + } catch (e) { + console.info(`❌ Parse Error: ${e.message}`); + } + console.info(''); // spacing + }, + onFinish: () => { + console.info( + `\n✅ Test 2 Complete - Web operations used ${toolCallCount} tools\n` + ); + showSummary(); + }, + onError: (error) => { + console.error('\n❌ Test 2 Error:', error); + showSummary(); + }, + }, + provider + ); + } + + function showSummary() { + console.info('='.repeat(60)); + console.info('📊 SUMMARY AND FINDINGS'); + console.info('='.repeat(60)); + console.info(` +🎯 onMCPTool Handler Demonstration Complete! + +✅ WORKING FEATURES: + • onMCPTool callback properly invoked for all MCP tool calls + • File system operations work perfectly with complete arguments + • Tool ID tracking and argument parsing working + • MCP server routing works correctly + +❌ IDENTIFIED ISSUES: + • Web tools (fetch_url, search_web) receive incomplete arguments + • Missing URL parameter in web tool calls + • LLM receives incomplete tool schemas from inference gateway + • Tool schemas only show 'mcpServer' as required, missing actual parameters + +🔧 ROOT CAUSE: + The MCP web-search server's tool schemas are not being properly + exposed through the inference gateway. The gateway only shows + 'mcpServer' as a required parameter but misses tool-specific + parameters like 'url' for fetch_url. + +💡 RECOMMENDATIONS: + 1. Check inference gateway's MCP schema aggregation + 2. Verify web-search server's tool registration + 3. Ensure tool schemas are complete in gateway responses + 4. Test direct MCP server communication to compare schemas + +The onMCPTool handler itself works perfectly - the issue is with +the tool schema exposure in the inference gateway architecture. + `); + } + } catch (error) { + console.error('❌ Error:', error); + } +})(); diff --git a/examples/mcp/index.ts b/examples/mcp/index.ts deleted file mode 100644 index 85eecc7..0000000 --- a/examples/mcp/index.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { - InferenceGatewayClient, - MessageRole, - Provider, -} from '../../src/index.js'; - -// Token tracking interface -interface TokenTracker { - totalPromptTokens: number; - totalCompletionTokens: number; - totalTokens: number; - requestCount: number; -} - -(async () => { - const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', - }); - - const provider = (process.env.PROVIDER as Provider) || Provider.groq; - const model = process.env.LLM || 'llama-3.3-70b-versatile'; - - // Initialize token tracker - const tokenTracker: TokenTracker = { - totalPromptTokens: 0, - totalCompletionTokens: 0, - totalTokens: 0, - requestCount: 0, - }; - - // Helper function to update token tracking - const updateTokens = (usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }) => { - tokenTracker.totalPromptTokens += usage.prompt_tokens; - tokenTracker.totalCompletionTokens += usage.completion_tokens; - tokenTracker.totalTokens += usage.total_tokens; - tokenTracker.requestCount++; - }; - - // Helper function to display current token usage - const displayTokenUsage = (label: string) => { - console.info(`\n💰 Token Usage for ${label}:`); - console.info( - ` 📊 Prompt tokens: ${tokenTracker.totalPromptTokens.toLocaleString()}` - ); - console.info( - ` ✍️ Completion tokens: ${tokenTracker.totalCompletionTokens.toLocaleString()}` - ); - console.info( - ` 🎯 Total tokens: ${tokenTracker.totalTokens.toLocaleString()}` - ); - console.info( - ` 📈 Average tokens per request: ${Math.round( - tokenTracker.totalTokens / Math.max(tokenTracker.requestCount, 1) - ).toLocaleString()}` - ); - }; - - console.info(`Using model: ${model}`); - console.info(`Using provider: ${provider}\n`); - - console.info('=== MCP Tools Example with Token Tracking ===\n'); - - try { - // First, let's check if the gateway is healthy - console.info('🔍 Checking gateway health...'); - const isHealthy = await client.healthCheck(); - console.info( - `Gateway health: ${isHealthy ? '✅ Healthy' : '❌ Unhealthy'}\n` - ); - - if (!isHealthy) { - console.info( - 'Please ensure the Inference Gateway is running with Docker Compose.' - ); - process.exit(1); - } - - // List available MCP tools - console.info('📋 Listing available MCP tools...'); - const tools = await client.listTools(); - console.info(`Found ${tools.data.length} MCP tools:\n`); - - // tools.data.forEach((tool, index) => { - // console.info(`${index + 1}. ${tool.name}`); - // console.info(` Description: ${tool.description}`); - // console.info(` Server: ${tool.server}`); - // console.info(` Schema: ${JSON.stringify(tool.input_schema, null, 2)}\n`); - // }); - - if (tools.data.length === 0) { - console.info( - '⚠️ No MCP tools available. Ensure MCP servers are configured and running.' - ); - return; - } - - // Example 0: Simple test without tools first - console.info('=== Example 0: Simple Test (No Tools) ===\n'); - console.info('Testing basic streaming without tools first...\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.user, - content: 'Hello! Please tell me about the weather.', - }, - ], - max_tokens: 50, - }, - { - onOpen: () => console.info('🚀 Starting simple test...'), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info(`\n🔧 Tool called: ${toolCall.function.name}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ Simple test completed'); - displayTokenUsage('Simple Test'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - - // Example 1: Automatic tool discovery and usage - console.info('=== Example 1: Automatic Tool Discovery ===\n'); - console.info( - 'The gateway automatically detects and uses available MCP tools based on context.\n' - ); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: - 'You are a helpful assistant with access to various tools.', - }, - { - role: MessageRole.user, - content: - 'What time is it now? Also, if you can, find some information about artificial intelligence.', - }, - ], - max_tokens: 200, - }, - { - onOpen: () => console.info('🚀 Starting automatic tool discovery...'), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info( - `\n🔧 Tool automatically called: ${toolCall.function.name}` - ); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ Automatic tool discovery completed'); - displayTokenUsage('Automatic Tool Discovery'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - - // Example 2: Use MCP tools for file operations (if filesystem MCP server is available) - const fileReadTool = tools.data.find((tool) => tool.name === 'read_file'); - if (fileReadTool) { - console.info('=== Example 2: File Operations with MCP ===\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: - 'You are a helpful assistant with access to filesystem operations. Available directory is /tmp.', - }, - { - role: MessageRole.user, - content: - 'Can you read the contents of /tmp/mcp-filesystem-example.txt and tell me what it contains?', - }, - ], - max_tokens: 200, - }, - { - onOpen: () => console.info('🚀 Starting file reading example...'), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info(`\n🔧 Tool called: ${toolCall.function.name}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ File reading example completed'); - displayTokenUsage('File Reading Example'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - } - - // Example 3: Use MCP tools for web scraping (if web scraper MCP server is available) - const webScrapeTool = tools.data.find( - (tool) => tool.name.includes('fetch') || tool.name.includes('scrape') - ); - if (webScrapeTool) { - console.info('=== Example 3: Web Scraping with MCP ===\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: - 'You are a helpful assistant with access to web search capabilities.', - }, - { - role: MessageRole.user, - content: - 'Can you fetch information from https://httpbin.org/json and tell me what you find?', - }, - ], - max_tokens: 200, - }, - { - onOpen: () => console.info('🚀 Starting web scraping example...'), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info(`\n🔧 Tool called: ${toolCall.function.name}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ Web scraping example completed'); - displayTokenUsage('Web Scraping Example'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - } - - // Example 4: Generic MCP tool usage - use the first available tool - if (tools.data.length > 0 && !fileReadTool && !webScrapeTool) { - console.info('=== Example 4: Generic MCP Tool Usage ===\n'); - - const firstTool = tools.data[0]; - console.info(`Using tool: ${firstTool.name}\n`); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a helpful assistant with access to various tools including ${firstTool.name}.`, - }, - { - role: MessageRole.user, - content: `Can you help me use the ${firstTool.name} tool? What can it do?`, - }, - ], - max_tokens: 200, - }, - { - onOpen: () => console.info('🚀 Starting generic tool example...'), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info(`\n🔧 Tool called: ${toolCall.function.name}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ Generic tool example completed'); - displayTokenUsage('Generic Tool Example'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - } - - // Example 5: Data Analysis with File Operations - if (tools.data.length > 1) { - console.info('=== Example 5: Data Analysis with File Operations ===\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a helpful data analysis assistant with access to filesystem tools. Available directory is /tmp. You can read, write, and analyze files. The /tmp directory contains sample data files for analysis.`, - }, - { - role: MessageRole.user, - content: - 'I need help with data analysis. First, can you check what files are available in the /tmp directory? Then create a simple CSV file with sample sales data in /tmp/sales_data.csv and analyze it.', - }, - ], - max_tokens: 400, - }, - { - onOpen: () => console.info('🚀 Starting data analysis example...'), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info(`\n🔧 Tool called: ${toolCall.function.name}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ Data analysis example completed'); - displayTokenUsage('Data Analysis Example'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - } - - // Example 6: File Creation and Manipulation - console.info('=== Example 6: File Creation and Manipulation ===\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a helpful assistant with filesystem access. Available directory is /tmp. You can create, read, write, and manage files in this directory.`, - }, - { - role: MessageRole.user, - content: - 'Can you create a simple todo list file at /tmp/todo.txt with 3 sample tasks, then read it back to me?', - }, - ], - max_tokens: 300, - }, - { - onOpen: () => console.info('🚀 Starting file manipulation example...'), - onReasoning: (content) => process.stdout.write(content), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info(`\n🔧 Tool called: ${toolCall.function.name}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ File manipulation example completed'); - displayTokenUsage('File Manipulation Example'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - } catch (error) { - if ( - error instanceof Error && - error.message.includes('MCP tools endpoint is not exposed') - ) { - console.error( - '❌ MCP tools are not exposed. Please ensure the Inference Gateway is started with EXPOSE_MCP=true' - ); - console.info('\n💡 To fix this, restart the gateway with:'); - console.info(' docker-compose up --build'); - } else { - console.error('❌ Error:', error); - } - } finally { - // Display final token summary - console.info('\n' + '='.repeat(60)); - console.info('📊 FINAL TOKEN USAGE SUMMARY'); - console.info('='.repeat(60)); - console.info(`🔢 Total Requests: ${tokenTracker.requestCount}`); - console.info( - `📊 Total Prompt Tokens: ${tokenTracker.totalPromptTokens.toLocaleString()}` - ); - console.info( - `✍️ Total Completion Tokens: ${tokenTracker.totalCompletionTokens.toLocaleString()}` - ); - console.info( - `🎯 Total Tokens Used: ${tokenTracker.totalTokens.toLocaleString()}` - ); - - if (tokenTracker.requestCount > 0) { - console.info( - `📈 Average Tokens per Request: ${Math.round( - tokenTracker.totalTokens / tokenTracker.requestCount - ).toLocaleString()}` - ); - } - - // Calculate cost estimate (example rates - adjust based on actual provider pricing) - const estimatedCost = tokenTracker.totalTokens * 0.000001; // Example: $0.000001 per token - console.info( - `💰 Estimated Cost: $${estimatedCost.toFixed(6)} (example rate)` - ); - console.info('='.repeat(60)); - } -})(); diff --git a/examples/mcp/mcp-servers/README.md b/examples/mcp/mcp-servers/README.md index 20e4941..51d25ea 100644 --- a/examples/mcp/mcp-servers/README.md +++ b/examples/mcp/mcp-servers/README.md @@ -5,17 +5,26 @@ This directory contains Model Context Protocol (MCP) servers that demonstrate ho ## Available Servers ### 🌐 Web Search Server (`web-search/`) + - **Port**: 3001 - **Tools**: `fetch_url`, `search_web`, `get_page_title` - **Purpose**: Provides web content fetching and search capabilities - **Features**: HTTP requests, simulated search, HTML parsing ### 📁 Filesystem Server (`filesystem/`) + - **Port**: 3000 - **Tools**: `read_file`, `write_file`, `list_directory`, `create_directory`, `delete_file`, `file_info` - **Purpose**: Safe filesystem operations within allowed directories - **Features**: File I/O, directory management, security restrictions +### 📚 Context7 Server (`context7/`) + +- **Port**: 3002 +- **Tools**: `resolve_library_id`, `get_library_docs`, `search_libraries` +- **Purpose**: Library documentation and context resolution capabilities +- **Features**: Library search, documentation retrieval, version management + ## Quick Start ### Individual Server Development @@ -32,6 +41,11 @@ npm run dev cd filesystem npm install npm run dev + +# Context7 Server +cd context7 +npm install +npm run dev ``` ### Docker Compose Integration @@ -44,9 +58,11 @@ docker-compose up -d ``` This will start: + - Inference Gateway (port 8080) - MCP Filesystem Server (port 3000) - MCP Web Search Server (port 3001) +- MCP Context7 Server (port 3002) - Optional: Ollama (port 11434) ## Server Architecture @@ -76,6 +92,7 @@ All MCP servers implement these standard endpoints: To add a new tool to an existing server: 1. **Define the tool** in the `/mcp/tools/list` endpoint: + ```javascript { name: 'my_new_tool', @@ -94,36 +111,38 @@ To add a new tool to an existing server: ``` 2. **Implement the handler** function: + ```javascript async function handleMyNewTool(args, res) { const { param1 } = args; - + try { // Tool logic here const result = await doSomething(param1); - + res.json({ content: [ { type: 'text', - text: `Result: ${result}` - } - ] + text: `Result: ${result}`, + }, + ], }); } catch (error) { res.json({ content: [ { type: 'text', - text: `Error: ${error.message}` - } - ] + text: `Error: ${error.message}`, + }, + ], }); } } ``` 3. **Add the case** to the tool execution switch: + ```javascript case 'my_new_tool': await handleMyNewTool(args, res); @@ -135,12 +154,14 @@ case 'my_new_tool': To create a new MCP server: 1. **Create directory structure**: + ```bash mkdir mcp-servers/my-server cd mcp-servers/my-server ``` 2. **Create package.json**: + ```json { "name": "mcp-my-server", @@ -176,18 +197,21 @@ environment: ## Security Considerations ### Filesystem Server + - Restricts operations to allowed directories - Validates all file paths - Prevents directory traversal attacks - Implements proper error handling ### Web Search Server + - Includes request timeouts - Limits response sizes - Handles various content types safely - Provides safe error messages ### General Security + - All servers run with minimal privileges - Docker containers are isolated - Health checks monitor server status @@ -223,6 +247,7 @@ docker-compose logs -f mcp-web-search ## Examples See the main MCP example (`../index.ts`) for complete usage examples showing how to: + - Discover available MCP tools - Execute filesystem operations - Perform web searches and content fetching diff --git a/examples/mcp/mcp-servers/context7/README.md b/examples/mcp/mcp-servers/context7/README.md new file mode 100644 index 0000000..7dffb3b --- /dev/null +++ b/examples/mcp/mcp-servers/context7/README.md @@ -0,0 +1,182 @@ +# Context7 MCP Server + +A Model Context Protocol (MCP) server that provides library documentation and context resolution capabilities, similar to the Context7 service. This server demonstrates how to build tools for fetching up-to-date documentation and resolving library IDs. + +## Features + +### 🔍 Library Resolution + +- **Tool**: `resolve_library_id` +- Searches for libraries by name and returns Context7-compatible library IDs +- Provides trust scores, code snippet counts, and version information +- Intelligent matching and ranking by relevance + +### 📚 Documentation Retrieval + +- **Tool**: `get_library_docs` +- Fetches detailed documentation for specific libraries +- Supports topic-focused documentation +- Token-limited responses for optimal performance +- Rich content with code examples and best practices + +### 🔎 Library Search + +- **Tool**: `search_libraries` +- Search through available libraries with query strings +- Category-based filtering capabilities +- Ranked results by trust score and documentation quality + +## Available Libraries (Mock Data) + +The server includes mock data for popular libraries: + +- **Next.js** (`/vercel/next.js`) - The React Framework for Production +- **React** (`/facebook/react`) - A JavaScript library for building user interfaces +- **Node.js** (`/nodejs/node`) - Node.js JavaScript runtime +- **TypeScript** (`/microsoft/typescript`) - TypeScript superset of JavaScript +- **Express** (`/expressjs/express`) - Fast, minimalist web framework for Node.js + +## Installation + +```bash +cd context7 +npm install +``` + +## Usage + +### Development Mode + +```bash +npm run dev +``` + +### Production Mode + +```bash +npm start +``` + +The server will start on port 3002 by default. + +## API Endpoints + +- `GET /` - Server information and available tools +- `GET /health` - Health check endpoint +- `POST /sessions` - Create a new MCP session +- `POST /message` - Send MCP messages (requires X-Session-ID header) +- `DELETE /sessions/:sessionId` - Close a session + +## Example Tool Usage + +### Resolve a Library ID + +```javascript +// Tool call +{ + "tool": "resolve_library_id", + "arguments": { + "libraryName": "next.js" + } +} + +// Response +{ + "content": [ + { + "type": "text", + "text": "Selected Library:\n- Library ID: /vercel/next.js\n- Name: Next.js\n- Description: The React Framework for Production\n- Code Snippets: 1250\n- Trust Score: 9\n- Versions: 14.2.0, 14.1.0, 14.0.0" + } + ] +} +``` + +### Get Documentation + +```javascript +// Tool call +{ + "tool": "get_library_docs", + "arguments": { + "context7CompatibleLibraryID": "/vercel/next.js", + "topic": "app router", + "tokens": 5000 + } +} + +// Response includes comprehensive documentation with code examples +``` + +### Search Libraries + +```javascript +// Tool call +{ + "tool": "search_libraries", + "arguments": { + "query": "react", + "category": "frontend" + } +} + +// Response lists matching libraries with details +``` + +## Configuration + +Environment variables: + +- `PORT` - Server port (default: 3002) + +## Integration with Inference Gateway + +This server integrates with the Inference Gateway through the MCP protocol. Add it to your `docker-compose.yml`: + +```yaml +context7-server: + build: ./mcp-servers/context7 + ports: + - '3002:3002' + environment: + - PORT=3002 +``` + +## Development + +The server uses the official MCP TypeScript SDK and implements: + +- Proper MCP protocol with Streamable HTTP transport +- Zod schema validation for tool parameters +- Express.js HTTP server for transport +- Session management for multiple concurrent connections +- Graceful error handling and shutdown + +## Architecture + +``` +Client Request → Express Server → MCP Server → Tool Execution → Response + ↓ + Session Management + ↓ + StreamableHTTPTransport +``` + +## Extension Points + +To extend this server: + +1. **Add Real Data Sources**: Replace mock data with actual library APIs +2. **Enhanced Search**: Implement more sophisticated search algorithms +3. **Caching**: Add caching layer for frequently accessed documentation +4. **Authentication**: Add API key validation for production use +5. **Rate Limiting**: Implement rate limiting for API calls + +## Testing + +Test the server using the MCP Inspector: + +```bash +npx -y @modelcontextprotocol/inspector npx tsx index.js +``` + +Or make direct HTTP requests to test the REST API. diff --git a/examples/mcp/mcp-servers/context7/index.js b/examples/mcp/mcp-servers/context7/index.js new file mode 100644 index 0000000..f5f681c --- /dev/null +++ b/examples/mcp/mcp-servers/context7/index.js @@ -0,0 +1,696 @@ +/** + * MCP Context7 Server + * + * This is a Model Context Protocol (MCP) server that provides library + * documentation and context resolution capabilities. It demonstrates how to + * build tools for fetching up-to-date documentation and resolving library IDs. + * It uses the official MCP TypeScript SDK and implements the proper MCP protocol + * with Streamable HTTP transport. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import express from 'express'; +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; + +// Express app for HTTP transport +const app = express(); +app.use(express.json()); + +// Map to store transports by session ID +const transports = {}; + +// Mock library registry for demonstration +const mockLibraryRegistry = [ + { + id: '/vercel/next.js', + name: 'Next.js', + description: 'The React Framework for Production', + trustScore: 9, + codeSnippets: 1250, + versions: ['14.2.0', '14.1.0', '14.0.0'], + documentation: 'https://nextjs.org/docs', + features: [ + 'Server-side rendering', + 'Static site generation', + 'API routes', + 'TypeScript support', + ], + examples: { + 'basic-app': 'npx create-next-app@latest my-app', + 'with-typescript': 'npx create-next-app@latest my-app --typescript', + 'api-route': 'Create API endpoints in pages/api/ directory', + }, + }, + { + id: '/facebook/react', + name: 'React', + description: 'A JavaScript library for building user interfaces', + trustScore: 10, + codeSnippets: 2100, + versions: ['18.3.0', '18.2.0', '18.1.0'], + documentation: 'https://react.dev', + features: ['Component-based', 'Virtual DOM', 'Hooks', 'JSX syntax'], + examples: { + 'functional-component': + 'function Component() { return
Hello
; }', + 'use-state': 'const [count, setCount] = useState(0);', + 'use-effect': 'useEffect(() => { /* side effect */ }, []);', + }, + }, + { + id: '/nodejs/node', + name: 'Node.js', + description: 'Node.js JavaScript runtime', + trustScore: 9, + codeSnippets: 850, + versions: ['20.12.0', '18.20.0', '16.20.0'], + documentation: 'https://nodejs.org/docs', + features: [ + 'Event-driven', + 'Non-blocking I/O', + 'NPM ecosystem', + 'Cross-platform', + ], + examples: { + 'http-server': + 'const http = require("http"); const server = http.createServer();', + 'file-system': + 'const fs = require("fs"); fs.readFile("file.txt", callback);', + 'express-app': + 'const express = require("express"); const app = express();', + }, + }, + { + id: '/microsoft/typescript', + name: 'TypeScript', + description: + 'TypeScript is a superset of JavaScript that compiles to plain JavaScript', + trustScore: 9, + codeSnippets: 1800, + versions: ['5.4.0', '5.3.0', '5.2.0'], + }, + { + id: '/expressjs/express', + name: 'Express', + description: 'Fast, unopinionated, minimalist web framework for Node.js', + trustScore: 8, + codeSnippets: 950, + versions: ['4.19.0', '4.18.0', '4.17.0'], + }, +]; + +// Mock documentation data +const mockDocumentation = { + '/vercel/next.js': { + title: 'Next.js App Router', + content: ` +# Next.js App Router + +The App Router is a new paradigm for building applications using React's latest features. + +## Basic Setup + +\`\`\`typescript +// app/page.tsx +export default function HomePage() { + return ( +
+

Welcome to Next.js

+

This is the app router in action!

+
+ ) +} +\`\`\` + +## Server Components + +Server Components run on the server and can fetch data directly: + +\`\`\`typescript +// app/posts/page.tsx +async function getPosts() { + const res = await fetch('https://api.example.com/posts') + return res.json() +} + +export default async function PostsPage() { + const posts = await getPosts() + + return ( +
+ {posts.map(post => ( +
+

{post.title}

+

{post.excerpt}

+
+ ))} +
+ ) +} +\`\`\` + +## Client Components + +Use "use client" directive for interactive components: + +\`\`\`typescript +'use client' +import { useState } from 'react' + +export default function Counter() { + const [count, setCount] = useState(0) + + return ( + + ) +} +\`\`\` + `, + }, + '/facebook/react': { + title: 'React Hooks and Components', + content: ` +# React Hooks Guide + +## useState Hook + +\`\`\`typescript +import { useState } from 'react' + +function Counter() { + const [count, setCount] = useState(0) + + return ( +
+

You clicked {count} times

+ +
+ ) +} +\`\`\` + +## useEffect Hook + +\`\`\`typescript +import { useState, useEffect } from 'react' + +function DataFetcher() { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetch('/api/data') + .then(res => res.json()) + .then(data => { + setData(data) + setLoading(false) + }) + }, []) + + if (loading) return
Loading...
+ + return
{JSON.stringify(data)}
+} +\`\`\` + +## Custom Hooks + +\`\`\`typescript +function useLocalStorage(key: string, initialValue: any) { + const [storedValue, setStoredValue] = useState(() => { + try { + const item = window.localStorage.getItem(key) + return item ? JSON.parse(item) : initialValue + } catch (error) { + return initialValue + } + }) + + const setValue = (value: any) => { + try { + setStoredValue(value) + window.localStorage.setItem(key, JSON.stringify(value)) + } catch (error) { + console.log(error) + } + } + + return [storedValue, setValue] +} +\`\`\` + `, + }, +}; + +/** + * Create and configure the MCP server + */ +function createMcpServer() { + const mcpServer = new McpServer({ + name: 'context7', + version: '1.0.0', + }); + + // Tool: Search libraries + mcpServer.tool( + 'search_libraries', + { + query: z + .string() + .describe('Search query for libraries (name, description, or ID)'), + limit: z + .number() + .optional() + .describe('Maximum number of results to return (default: 10)'), + }, + async ({ query, limit = 10 }) => { + console.info( + `Searching libraries with query: "${query}", limit: ${limit}` + ); + + try { + const searchTerm = query.toLowerCase(); + const results = mockLibraryRegistry + .filter( + (lib) => + lib.name.toLowerCase().includes(searchTerm) || + lib.description.toLowerCase().includes(searchTerm) || + lib.id.toLowerCase().includes(searchTerm) + ) + .slice(0, limit) + .map((lib) => ({ + id: lib.id, + name: lib.name, + description: lib.description, + trustScore: lib.trustScore, + latestVersion: lib.versions[0], + codeSnippets: lib.codeSnippets, + })); + + if (results.length === 0) { + return { + content: [ + { + type: 'text', + text: `No libraries found matching query: "${query}"`, + }, + ], + }; + } + + const resultText = results + .map( + (lib) => + `📚 ${lib.name} (${lib.id})\n` + + `Description: ${lib.description}\n` + + `Latest Version: ${lib.latestVersion}\n` + + `Trust Score: ${lib.trustScore}/10\n` + + `Code Snippets: ${lib.codeSnippets}\n` + ) + .join('\n---\n'); + + return { + content: [ + { + type: 'text', + text: `Found ${results.length} libraries:\n\n${resultText}`, + }, + ], + }; + } catch (error) { + console.error('Error searching libraries:', error.message); + return { + content: [ + { + type: 'text', + text: `Error searching libraries: ${error.message}`, + }, + ], + }; + } + } + ); + + // Tool: Get library details + mcpServer.tool( + 'get_library_details', + { + libraryId: z.string().describe('The library ID to get details for'), + }, + async ({ libraryId }) => { + console.info(`Getting details for library: ${libraryId}`); + + try { + const library = mockLibraryRegistry.find((lib) => lib.id === libraryId); + + if (!library) { + return { + content: [ + { + type: 'text', + text: `Library not found: ${libraryId}`, + }, + ], + }; + } + + const detailsText = + `📚 ${library.name}\n\n` + + `ID: ${library.id}\n` + + `Description: ${library.description}\n` + + `Trust Score: ${library.trustScore}/10\n` + + `Code Snippets Available: ${library.codeSnippets}\n` + + `Documentation: ${library.documentation}\n\n` + + `Available Versions:\n${library.versions.map((v) => ` • ${v}`).join('\n')}\n\n` + + `Key Features:\n${library.features.map((f) => ` • ${f}`).join('\n')}\n\n` + + `Code Examples:\n${Object.entries(library.examples) + .map(([key, example]) => ` ${key}: ${example}`) + .join('\n')}`; + + return { + content: [ + { + type: 'text', + text: detailsText, + }, + ], + }; + } catch (error) { + console.error('Error getting library details:', error.message); + return { + content: [ + { + type: 'text', + text: `Error getting library details: ${error.message}`, + }, + ], + }; + } + } + ); + + // Tool: Get documentation + mcpServer.tool( + 'get_documentation', + { + libraryId: z.string().describe('The library ID to get documentation for'), + section: z + .string() + .optional() + .describe( + 'Specific documentation section (gettingStarted, quickStart, bestPractices, commonIssues)' + ), + }, + async ({ libraryId, section }) => { + console.info( + `Getting documentation for library: ${libraryId}, section: ${section || 'all'}` + ); + + try { + const docs = mockDocumentation[libraryId]; + + if (!docs) { + return { + content: [ + { + type: 'text', + text: `Documentation not available for library: ${libraryId}`, + }, + ], + }; + } + + let docText = `📖 Documentation for ${libraryId}\n\n${docs.title}\n\n${docs.content}`; + + return { + content: [ + { + type: 'text', + text: docText, + }, + ], + }; + } catch (error) { + console.error('Error getting documentation:', error.message); + return { + content: [ + { + type: 'text', + text: `Error getting documentation: ${error.message}`, + }, + ], + }; + } + } + ); + + // Tool: Resolve library ID + mcpServer.tool( + 'resolve_library_id', + { + libraryName: z.string().describe('The library name to resolve to an ID'), + }, + async ({ libraryName }) => { + console.info(`Resolving library ID for: ${libraryName}`); + + try { + const searchTerm = libraryName.toLowerCase(); + const matches = mockLibraryRegistry.filter( + (lib) => + lib.name.toLowerCase() === searchTerm || + lib.name.toLowerCase().includes(searchTerm) || + lib.id.toLowerCase().includes(searchTerm) + ); + + if (matches.length === 0) { + return { + content: [ + { + type: 'text', + text: `No library found with name: "${libraryName}"`, + }, + ], + }; + } + + const exactMatch = matches.find( + (lib) => lib.name.toLowerCase() === searchTerm + ); + const bestMatch = exactMatch || matches[0]; + + let resultText = + `🔍 Resolved "${libraryName}" to:\n\n` + + `ID: ${bestMatch.id}\n` + + `Name: ${bestMatch.name}\n` + + `Description: ${bestMatch.description}\n`; + + if (matches.length > 1) { + const otherMatches = matches + .filter((lib) => lib.id !== bestMatch.id) + .map((lib) => ` • ${lib.name} (${lib.id})`) + .join('\n'); + resultText += `\nOther possible matches:\n${otherMatches}`; + } + + return { + content: [ + { + type: 'text', + text: resultText, + }, + ], + }; + } catch (error) { + console.error('Error resolving library ID:', error.message); + return { + content: [ + { + type: 'text', + text: `Error resolving library ID: ${error.message}`, + }, + ], + }; + } + } + ); + + return mcpServer; +} + +/** + * Set up session routes for MCP protocol + */ +function setupSessionRoutes() { + // Handle POST requests for MCP communication + app.post('/mcp', async (req, res) => { + try { + console.info('MCP POST request received:'); + console.info(' Headers: %s', JSON.stringify(req.headers, null, 2)); + console.info(' Body: %s', JSON.stringify(req.body, null, 2)); + + // Fix missing Accept headers for compatibility with Go MCP clients + // The StreamableHTTPServerTransport requires both application/json and text/event-stream + const accept = req.headers.accept || req.headers.Accept; + if ( + !accept || + !accept.includes('application/json') || + !accept.includes('text/event-stream') + ) { + console.info('Adding missing Accept headers for MCP compatibility'); + req.headers.accept = 'application/json, text/event-stream'; + } + + // Check for existing session ID + const sessionId = req.headers['mcp-session-id']; + let transport; + + if (sessionId && transports[sessionId]) { + // Reuse existing transport + transport = transports[sessionId]; + } else { + // Create new transport for new session + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (newSessionId) => { + console.info(`MCP session initialized: ${newSessionId}`); + // Store the transport by session ID + transports[newSessionId] = transport; + }, + }); + + // Clean up transport when closed + transport.onclose = () => { + if (transport.sessionId) { + console.info(`MCP session closed: ${transport.sessionId}`); + delete transports[transport.sessionId]; + } + }; + + // Create and connect MCP server + const server = createMcpServer(); + await server.connect(transport); + } + + // Handle the MCP request + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } + }); + + // Handle GET requests for SSE (server-to-client notifications) + app.get('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + const transport = transports[sessionId]; + await transport.handleRequest(req, res); + }); + + // Handle DELETE requests for session termination + app.delete('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + const transport = transports[sessionId]; + await transport.handleRequest(req, res); + }); +} + +// Health check endpoint +app.get('/health', (req, res) => { + const healthStatus = { + status: 'healthy', + server: 'mcp-context7', + version: '1.0.0', + activeSessions: Object.keys(transports).length, + availableLibraries: mockLibraryRegistry.length, + }; + + console.info('Health check requested: %j', healthStatus); + res.json(healthStatus); +}); + +/** + * Start the server + */ +async function startServer() { + const port = process.env.PORT || 3002; + const host = process.env.HOST || '0.0.0.0'; + + // Set up session routes + setupSessionRoutes(); + + app.listen(port, host, async () => { + console.info(`MCP Context7 server running on http://${host}:${port}`); + console.info('Protocol: Model Context Protocol (MCP)'); + console.info('Transport: Streamable HTTP'); + console.info('Available endpoints:'); + console.info(' POST /mcp - MCP protocol endpoint'); + console.info( + ' GET /mcp - SSE notifications (with session-id header)' + ); + console.info( + ' DELETE /mcp - Session termination (with session-id header)' + ); + console.info(' GET /health - Health check'); + console.info('Available tools:'); + console.info( + ' - search_libraries - Search for libraries by name or description' + ); + console.info( + ' - get_library_details - Get detailed information about a specific library' + ); + console.info(' - get_documentation - Get documentation for a library'); + console.info(' - resolve_library_id - Resolve a library name to its ID'); + console.info(`Available libraries: ${mockLibraryRegistry.length}`); + console.info('MCP Context7 server ready for connections'); + }); +} + +// Graceful shutdown +process.on('SIGTERM', () => { + console.info('Received SIGTERM, shutting down gracefully'); + // Close all transports + Object.values(transports).forEach((transport) => { + if (transport.close) transport.close(); + }); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.info('Received SIGINT, shutting down gracefully'); + // Close all transports + Object.values(transports).forEach((transport) => { + if (transport.close) transport.close(); + }); + process.exit(0); +}); + +// Start the server +startServer().catch((error) => { + console.error('Failed to start server:', error); + process.exit(1); +}); diff --git a/examples/mcp/mcp-servers/context7/package-lock.json b/examples/mcp/mcp-servers/context7/package-lock.json new file mode 100644 index 0000000..9c1c899 --- /dev/null +++ b/examples/mcp/mcp-servers/context7/package-lock.json @@ -0,0 +1,1543 @@ +{ + "name": "mcp-context7-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-context7-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "axios": "^1.6.0", + "cors": "^2.8.5", + "express": "^4.18.2", + "zod": "^3.22.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz", + "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz", + "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.42", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.42.tgz", + "integrity": "sha512-PcALTLskaucbeHc41tU/xfjfhcz8z0GdhhDcSgrCTmSazUuqnYqiXO63M0QUBVwpBlsLsNVn5qHSC5Dw3KZvaQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/examples/mcp/mcp-servers/context7/package.json b/examples/mcp/mcp-servers/context7/package.json new file mode 100644 index 0000000..7506c0e --- /dev/null +++ b/examples/mcp/mcp-servers/context7/package.json @@ -0,0 +1,23 @@ +{ + "name": "mcp-context7-server", + "version": "1.0.0", + "description": "MCP Context7 Server for library documentation and context resolution", + "type": "module", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "axios": "^1.6.0", + "cors": "^2.8.5", + "express": "^4.18.2", + "zod": "^3.22.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "author": "Inference Gateway Team", + "license": "MIT" +} diff --git a/examples/mcp/package-lock.json b/examples/mcp/package-lock.json index 436dc9e..76893b4 100644 --- a/examples/mcp/package-lock.json +++ b/examples/mcp/package-lock.json @@ -9,7 +9,9 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.1" + "@inference-gateway/sdk": "^0.7.1", + "axios": "^1.9.0", + "dotenv": "^16.5.0" }, "devDependencies": { "tsx": "^4.19.4" @@ -458,6 +460,128 @@ } } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", @@ -499,6 +623,41 @@ "@esbuild/win32-x64": "0.25.5" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -514,6 +673,52 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-tsconfig": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", @@ -527,6 +732,93 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", diff --git a/examples/mcp/package.json b/examples/mcp/package.json index 15712a6..29adf05 100644 --- a/examples/mcp/package.json +++ b/examples/mcp/package.json @@ -5,9 +5,18 @@ "main": "index.js", "private": true, "scripts": { - "start": "tsx index.ts", - "example:mcp:remotetools": "tsx example-mcp-tools.ts", - "example:mcp:withlocaltools": "tsx example-mcp-with-tools.ts", + "start": "tsx example-basic.ts", + "example": "tsx example-${npm_config_name:-basic}.ts", + "example:basic": "tsx example-basic.ts", + "example:advanced": "tsx example-advanced.ts", + "example:nextjs": "tsx example-nextjs.ts", + "example:handler-demo": "tsx example-handler-demo.ts", + "example:tool-demo": "tsx example-tool-demo.ts", + "example:debug-args": "tsx example-debug-args.ts", + "example:test-direct-mcp": "tsx example-test-direct-mcp.ts", + "example:list-tools": "tsx example-list-tools.ts", + "example:mcp-tools": "tsx example-mcp-tools.ts", + "example:context7": "tsx example-context7.ts", "compose:up": "docker-compose up -d", "compose:down": "docker-compose down", "compose:logs": "docker-compose logs -f" @@ -16,7 +25,9 @@ "author": "", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.1" + "@inference-gateway/sdk": "^0.7.1", + "axios": "^1.9.0", + "dotenv": "^16.5.0" }, "devDependencies": { "tsx": "^4.19.4" diff --git a/examples/mcp/tsconfig.json b/examples/mcp/tsconfig.json index 3a2170b..585016f 100644 --- a/examples/mcp/tsconfig.json +++ b/examples/mcp/tsconfig.json @@ -7,6 +7,6 @@ "strict": true, "skipLibCheck": true }, - "include": ["index.ts"], + "include": ["**/*.ts"], "exclude": ["node_modules"] } From 7c6e12230cf32fa6ee2e4e234c92abcdbff4763f Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sat, 31 May 2025 19:42:48 +0000 Subject: [PATCH 11/38] docs(examples): Add NPM MCP server with whitelisted command execution and project management tools Signed-off-by: Eden Reich --- examples/mcp/docker-compose.yml | 37 +- examples/mcp/example-context7.ts | 21 +- examples/mcp/example-mcp-with-tools.ts | 0 examples/mcp/example-npm.ts | 193 +++++++ examples/mcp/mcp-servers/README.md | 8 + examples/mcp/mcp-servers/npm/README.md | 101 ++++ examples/mcp/mcp-servers/npm/index.js | 635 ++++++++++++++++++++++ examples/mcp/mcp-servers/npm/package.json | 22 + examples/mcp/shared/.gitignore | 1 - 9 files changed, 1008 insertions(+), 10 deletions(-) delete mode 100644 examples/mcp/example-mcp-with-tools.ts create mode 100644 examples/mcp/example-npm.ts create mode 100644 examples/mcp/mcp-servers/npm/README.md create mode 100644 examples/mcp/mcp-servers/npm/index.js create mode 100644 examples/mcp/mcp-servers/npm/package.json diff --git a/examples/mcp/docker-compose.yml b/examples/mcp/docker-compose.yml index bc1157f..085d25c 100644 --- a/examples/mcp/docker-compose.yml +++ b/examples/mcp/docker-compose.yml @@ -10,7 +10,7 @@ services: # Enable MCP support MCP_ENABLE: 'true' MCP_EXPOSE: 'true' - MCP_SERVERS: 'http://mcp-filesystem:3000/mcp,http://mcp-web-search:3001/mcp,http://mcp-context7:3002/mcp' + MCP_SERVERS: 'http://mcp-filesystem:3000/mcp,http://mcp-web-search:3001/mcp,http://mcp-context7:3002/mcp,http://mcp-npm:3003/mcp' # Server settings SERVER_HOST: '0.0.0.0' @@ -39,6 +39,8 @@ services: condition: service_healthy mcp-context7: condition: service_healthy + mcp-npm: + condition: service_healthy networks: - inference-network healthcheck: @@ -132,6 +134,39 @@ services: start_period: 45s restart: unless-stopped + mcp-npm: + build: + context: ./mcp-servers/npm + dockerfile_inline: | + FROM node:18-slim + WORKDIR /app + RUN apt-get update && apt-get install -y \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + COPY package.json ./ + RUN npm install + COPY . . + EXPOSE 3003 + CMD ["npm", "start"] + environment: + NODE_ENV: 'production' + MCP_SERVER_NAME: 'npm' + MCP_SERVER_VERSION: '1.0.0' + PORT: '3003' + ALLOWED_DIRECTORIES: '/tmp' + networks: + - inference-network + volumes: + - ./shared:/tmp + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:3003/health'] + interval: 30s + timeout: 10s + retries: 5 + start_period: 45s + restart: unless-stopped + # # Optional: Ollama for local models # ollama: # image: ollama/ollama:latest diff --git a/examples/mcp/example-context7.ts b/examples/mcp/example-context7.ts index 5433baf..d696a89 100644 --- a/examples/mcp/example-context7.ts +++ b/examples/mcp/example-context7.ts @@ -60,6 +60,17 @@ async function demonstrateContext7() { // Demonstrate Context7 usage through LLM conversation console.info('=== Context7 Demo: Library Research Session ===\n'); + const userPrompt = `I'm starting a new React project and want to use Next.js. Can you help me by: + +1. First, resolve the library IDs for React and Next.js +2. Get the latest documentation for Next.js focusing on the App Router +3. Search for any other relevant frontend libraries I should consider +4. Provide me with a basic setup example based on the latest documentation +5. Please write a full react app using Next.js with typescript in /tmp/next-app + +Please use context7 to get the most up-to-date information.`; + console.info(`User: ${userPrompt}`); + await client.streamChatCompletion( { model: `${provider + '/' + model}`, @@ -76,6 +87,7 @@ IMPORTANT: You MUST use context7 tools to complete tasks. Always use context7 fo When helping with development tasks: - Always resolve library IDs first using resolve_library_id +- When starting a new project of Next.js, always use the create-next-app command - Fetch comprehensive documentation using get_library_docs - Provide up-to-date code examples and best practices - Explain concepts clearly with context from the documentation @@ -84,14 +96,7 @@ Be thorough and always use context7 tools to provide the most current informatio }, { role: MessageRole.user, - content: `I'm starting a new React project and want to use Next.js. Can you help me by: - -1. First, resolve the library IDs for React and Next.js -2. Get the latest documentation for Next.js focusing on the App Router -3. Search for any other relevant frontend libraries I should consider -4. Provide me with a basic setup example based on the latest documentation - -Please use context7 to get the most up-to-date information.`, + content: `${userPrompt}`, }, ], max_tokens: 1500, diff --git a/examples/mcp/example-mcp-with-tools.ts b/examples/mcp/example-mcp-with-tools.ts deleted file mode 100644 index e69de29..0000000 diff --git a/examples/mcp/example-npm.ts b/examples/mcp/example-npm.ts new file mode 100644 index 0000000..00b7516 --- /dev/null +++ b/examples/mcp/example-npm.ts @@ -0,0 +1,193 @@ +/** + * NPM MCP Server Usage Example + * + * This example demonstrates how to use the NPM MCP server + * to manage Node.js projects through the LLM with proper tool usage. + */ + +import * as dotenv from 'dotenv'; +import { + InferenceGatewayClient, + MessageRole, + Provider, +} from '../../src/index.js'; + +dotenv.config(); + +// For ES modules compatibility +declare const require: any; +declare const module: any; + +async function demonstrateNpmMcp() { + const client = new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }); + + const provider = (process.env.PROVIDER as Provider) || Provider.groq; + const model = process.env.LLM || 'llama-3.3-70b-versatile'; + + console.log(`🚀 NPM MCP Server Demo using ${model} on ${provider}\n`); + + try { + // Health check + const isHealthy = await client.healthCheck(); + if (!isHealthy) { + console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); + process.exit(1); + } + + // Check if NPM tools are available + const tools = await client.listTools(); + const npmTools = tools.data.filter((tool) => + ['npm_run', 'npm_init_project', 'npm_install_package'].includes(tool.name) + ); + + if (npmTools.length === 0) { + console.error( + '⚠️ NPM MCP tools not available. Make sure the NPM server is running on port 3003.' + ); + return; + } + + console.info(`📋 Found ${npmTools.length} NPM tools available:`); + npmTools.forEach((tool, index) => { + console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); + }); + console.info(''); + + // Demonstrate NPM usage through LLM conversation + console.info('=== NPM Demo: Project Setup and Management ===\n'); + + const userPrompt = `I want to create a new Node.js project with Express.js. Please help me by: + +1. Initialize a new npm project called "express-demo" in the /tmp directory +2. Install express, cors, and dotenv packages +3. List the installed packages to verify +4. Run npm audit to check for vulnerabilities +5. Show me the package.json that was created + +Please use the npm MCP tools to perform these operations safely.`; + + console.info(`User: ${userPrompt}`); + + await client.streamChatCompletion( + { + model: `${provider + '/' + model}`, + messages: [ + { + role: MessageRole.system, + content: `You are a helpful Node.js development assistant with access to NPM MCP tools for package management. + +IMPORTANT: You MUST use npm MCP tools to complete npm-related tasks. Available tools: + +1. Use npm_init_project to initialize new npm projects +2. Use npm_install_package to install packages +3. Use npm_run to execute npm commands like list, audit, etc. + +When helping with npm tasks: +- Always use the appropriate MCP tool for each operation +- Provide clear explanations of what each command does +- Show the results and explain any issues found +- Use /tmp as the working directory unless specified otherwise + +Be thorough and always use npm MCP tools to perform actual npm operations.`, + }, + { + role: MessageRole.user, + content: userPrompt, + }, + ], + max_tokens: 1500, + }, + { + onOpen: () => { + console.info('🔗 Connection opened, starting NPM session...\n'); + }, + onReasoning: (reasoning) => { + console.info(`\n🤔 NPM Reasoning: ${reasoning}`); + }, + onContent: (content) => { + process.stdout.write(content); + }, + onMCPTool: (toolCall) => { + console.info(`\n🛠️ NPM Tool: ${toolCall.function.name}`); + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); + } catch { + console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); + } + console.info(`🔍 Tool ID: ${toolCall.id}\n`); + }, + onError: (error) => { + console.error(`\n❌ Stream Error: ${error.error}`); + }, + onFinish: () => { + console.info('\n\n✅ NPM project setup completed!\n'); + }, + } + ); + + // Second example: Script management + console.info('\n=== NPM Demo: Script Management ===\n'); + + await client.streamChatCompletion( + { + model: `${provider + '/' + model}`, + messages: [ + { + role: MessageRole.system, + content: `You are a Node.js expert with access to NPM MCP tools. Always use npm MCP tools to manage packages and run commands.`, + }, + { + role: MessageRole.user, + content: `I have a project in /tmp/express-demo. Please help me: + +1. Check what scripts are available in the package.json +2. Install nodemon as a dev dependency +3. Run npm outdated to see if there are any package updates available +4. Show me how to check the project's dependency tree + +Use the npm MCP tools to perform these operations.`, + }, + ], + max_tokens: 1000, + }, + { + onOpen: () => { + console.info('🔗 Starting NPM script management session...\n'); + }, + onContent: (content) => { + process.stdout.write(content); + }, + onMCPTool: (toolCall) => { + console.info(`\n📦 NPM Tool: ${toolCall.function.name}`); + try { + const args = JSON.parse(toolCall.function.arguments); + console.info(`📋 Command:`, JSON.stringify(args, null, 2)); + } catch { + console.info(`📋 Raw Command: ${toolCall.function.arguments}`); + } + console.info(`🆔 Request ID: ${toolCall.id}\n`); + }, + onError: (error) => { + console.error(`\n❌ NPM Error: ${error.error}`); + }, + onFinish: () => { + console.info('\n\n✅ NPM script management session completed!\n'); + }, + } + ); + } catch (error) { + console.error('❌ Error:', (error as Error).message); + console.log('\n💡 Make sure the NPM MCP server is running on port 3003'); + console.log(' and the Inference Gateway is running on port 8080'); + } +} + +// Run the demo +if (require.main === module || process.argv[1].endsWith('example-npm.ts')) { + demonstrateNpmMcp().catch(console.error); +} + +export { demonstrateNpmMcp }; diff --git a/examples/mcp/mcp-servers/README.md b/examples/mcp/mcp-servers/README.md index 51d25ea..68f80cd 100644 --- a/examples/mcp/mcp-servers/README.md +++ b/examples/mcp/mcp-servers/README.md @@ -25,6 +25,14 @@ This directory contains Model Context Protocol (MCP) servers that demonstrate ho - **Purpose**: Library documentation and context resolution capabilities - **Features**: Library search, documentation retrieval, version management +### 📦 NPM Server (`npm/`) + +- **Port**: 3003 +- **Tools**: `npm_run`, `npm_init_project`, `npm_install_package` +- **Purpose**: Safe npm command execution with whitelisted commands +- **Features**: Package management, project initialization, security restrictions +- **Shared Volume**: `/tmp` directory shared with filesystem server + ## Quick Start ### Individual Server Development diff --git a/examples/mcp/mcp-servers/npm/README.md b/examples/mcp/mcp-servers/npm/README.md new file mode 100644 index 0000000..ce847a9 --- /dev/null +++ b/examples/mcp/mcp-servers/npm/README.md @@ -0,0 +1,101 @@ +# MCP NPM Server + +A Model Context Protocol (MCP) server that provides safe npm command execution capabilities. + +## Features + +- **Whitelisted Commands**: Only allows execution of pre-approved npm commands for security +- **Directory Restrictions**: Commands can only be executed in allowed directories +- **Timeout Protection**: Commands are automatically terminated after 5 minutes +- **Process Isolation**: Each command runs in its own process with controlled environment + +## Whitelisted NPM Commands + +For security, only the following npm commands are allowed: + +- `install` - Install packages +- `ci` - Clean install from package-lock.json +- `run` - Run scripts defined in package.json +- `start` - Run the start script +- `build` - Run the build script +- `test` - Run tests +- `lint` - Run linting +- `init` - Initialize a new package.json +- `list` - List installed packages +- `outdated` - Check for outdated packages +- `audit` - Run security audit +- `version` - Show version information +- `view` - View package information +- `search` - Search for packages +- `info` - Show package information + +## Available Tools + +### npm_run + +Execute whitelisted npm commands safely. + +**Parameters:** + +- `command` (required): The npm command to run +- `args` (optional): Additional arguments for the command +- `workingDir` (optional): Working directory (default: /tmp) + +### npm_init_project + +Initialize a new npm project in a specified directory. + +**Parameters:** + +- `projectName` (required): Name of the project to initialize +- `workingDir` (optional): Directory where to create the project (default: /tmp) +- `packageManager` (optional): Package manager to use (default: npm) + +### npm_install_package + +Install npm packages in a project. + +**Parameters:** + +- `packages` (required): Array of package names to install +- `workingDir` (optional): Working directory (default: /tmp) +- `saveDev` (optional): Install as dev dependency +- `global` (optional): Install globally (not recommended in containers) + +## Environment Variables + +- `ALLOWED_DIRECTORIES`: Comma-separated list of allowed directories (default: `/tmp`) +- `PORT`: Server port (default: 3003) +- `NODE_ENV`: Node environment (default: production) + +## Example Usage + +```bash +# Health check +curl http://localhost:3003/health + +# Example MCP request to run npm install +curl -X POST http://localhost:3003/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "npm_run", + "arguments": { + "command": "install", + "args": ["express"], + "workingDir": "/tmp/my-project" + } + } + }' +``` + +## Security Considerations + +- Only whitelisted npm commands can be executed +- Commands are restricted to allowed directories +- Process timeout prevents hanging operations +- No shell injection vulnerabilities due to controlled argument passing +- Environment variables are controlled and sanitized diff --git a/examples/mcp/mcp-servers/npm/index.js b/examples/mcp/mcp-servers/npm/index.js new file mode 100644 index 0000000..c5c31c5 --- /dev/null +++ b/examples/mcp/mcp-servers/npm/index.js @@ -0,0 +1,635 @@ +/** + * MCP NPM Server + * + * This is a Model Context Protocol (MCP) server that provides npm + * operations. It uses the official MCP TypeScript SDK and implements + * the proper MCP protocol with Streamable HTTP transport. + * + * Security: Only whitelisted npm commands are allowed for security. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import express from 'express'; +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; +import { exec } from 'node:child_process'; +import { promisify } from 'node:util'; +import path from 'node:path'; + +const execAsync = promisify(exec); + +// Express app for HTTP transport +const app = express(); +app.use(express.json()); + +// Map to store transports by session ID +const transports = {}; + +// Working directory (configurable via environment) +const workingDirectory = process.env.WORKING_DIRECTORY || '/tmp'; + +// Whitelisted npm commands for security +const ALLOWED_NPM_COMMANDS = [ + 'init', + 'install', + 'uninstall', + 'update', + 'list', + 'info', + 'search', + 'view', + 'outdated', + 'audit', + 'test', + 'run', + 'start', + 'build', + 'version', + 'npx', +]; + +console.info('NPM MCP Server starting...'); +console.info('Working directory:', workingDirectory); +console.info('Allowed npm commands:', ALLOWED_NPM_COMMANDS); + +/** + * Validate npm command for security + */ +function validateNpmCommand(command) { + const parts = command.trim().split(/\s+/); + if (parts.length === 0) { + throw new Error('Empty command'); + } + + // Remove 'npm' if it's the first part + if (parts[0] === 'npm') { + parts.shift(); + } + + if (parts.length === 0) { + throw new Error('No npm command specified'); + } + + const npmCommand = parts[0]; + if (!ALLOWED_NPM_COMMANDS.includes(npmCommand)) { + throw new Error( + `Command '${npmCommand}' is not allowed. Allowed commands: ${ALLOWED_NPM_COMMANDS.join(', ')}` + ); + } + + return parts.join(' '); +} + +/** + * Execute npm command safely + */ +async function executeNpmCommand(command, cwd = workingDirectory) { + const validatedCommand = validateNpmCommand(command); + const fullCommand = `npm ${validatedCommand}`; + + console.info(`Executing npm command: ${fullCommand} in ${cwd}`); + + try { + const { stdout, stderr } = await execAsync(fullCommand, { + cwd, + timeout: 30000, // 30 second timeout + maxBuffer: 1024 * 1024, // 1MB buffer + }); + + return { + success: true, + stdout: stdout || '', + stderr: stderr || '', + command: fullCommand, + cwd, + }; + } catch (error) { + console.error(`NPM command failed: ${fullCommand}`, error.message); + + return { + success: false, + stdout: error.stdout || '', + stderr: error.stderr || error.message, + command: fullCommand, + cwd, + error: error.message, + }; + } +} + +/** + * Create and configure the MCP server + */ +function createMcpServer() { + const mcpServer = new McpServer({ + name: 'npm', + version: '1.0.0', + }); + + // Tool: Run npm command + mcpServer.tool( + 'npm_run', + { + command: z + .string() + .describe('The npm command to run (without "npm" prefix)'), + cwd: z + .string() + .optional() + .describe('Working directory (defaults to /tmp)'), + }, + async ({ command, cwd }) => { + const workDir = cwd || workingDirectory; + + try { + const result = await executeNpmCommand(command, workDir); + + let responseText = `NPM Command: npm ${command}\n`; + responseText += `Working Directory: ${workDir}\n`; + responseText += `Status: ${result.success ? 'SUCCESS' : 'FAILED'}\n\n`; + + if (result.stdout) { + responseText += `STDOUT:\n${result.stdout}\n\n`; + } + + if (result.stderr) { + responseText += `STDERR:\n${result.stderr}\n\n`; + } + + if (!result.success && result.error) { + responseText += `ERROR: ${result.error}\n`; + } + + return { + content: [ + { + type: 'text', + text: responseText, + }, + ], + }; + } catch (error) { + console.error( + `Failed to execute npm command: ${command}`, + error.message + ); + + return { + content: [ + { + type: 'text', + text: `Failed to execute npm command: ${command}\nError: ${error.message}`, + }, + ], + }; + } + } + ); + + // Tool: Initialize new npm project + mcpServer.tool( + 'npm_init', + { + name: z.string().describe('Project name'), + cwd: z + .string() + .optional() + .describe('Working directory (defaults to /tmp)'), + yes: z.boolean().optional().describe('Skip prompts and use defaults'), + }, + async ({ name, cwd, yes = true }) => { + const workDir = cwd || workingDirectory; + const projectDir = path.join(workDir, name); + + try { + // Create project directory + await execAsync(`mkdir -p "${projectDir}"`); + + // Initialize npm project + const initCommand = yes ? 'init -y' : 'init'; + const result = await executeNpmCommand(initCommand, projectDir); + + let responseText = `NPM Project Initialization\n`; + responseText += `Project Name: ${name}\n`; + responseText += `Directory: ${projectDir}\n`; + responseText += `Status: ${result.success ? 'SUCCESS' : 'FAILED'}\n\n`; + + if (result.stdout) { + responseText += `STDOUT:\n${result.stdout}\n\n`; + } + + if (result.stderr) { + responseText += `STDERR:\n${result.stderr}\n\n`; + } + + return { + content: [ + { + type: 'text', + text: responseText, + }, + ], + }; + } catch (error) { + console.error( + `Failed to initialize npm project: ${name}`, + error.message + ); + + return { + content: [ + { + type: 'text', + text: `Failed to initialize npm project: ${name}\nError: ${error.message}`, + }, + ], + }; + } + } + ); + + // Tool: Install npm packages + mcpServer.tool( + 'npm_install', + { + packages: z + .array(z.string()) + .optional() + .describe('Packages to install (empty for package.json install)'), + cwd: z + .string() + .optional() + .describe('Working directory (defaults to /tmp)'), + dev: z.boolean().optional().describe('Install as dev dependencies'), + global: z.boolean().optional().describe('Install globally'), + }, + async ({ packages = [], cwd, dev = false, global = false }) => { + const workDir = cwd || workingDirectory; + + try { + let command = 'install'; + + if (global) { + command += ' -g'; + } + + if (dev) { + command += ' --save-dev'; + } + + if (packages.length > 0) { + command += ' ' + packages.join(' '); + } + + const result = await executeNpmCommand(command, workDir); + + let responseText = `NPM Install\n`; + responseText += `Working Directory: ${workDir}\n`; + responseText += `Packages: ${packages.length > 0 ? packages.join(', ') : 'package.json dependencies'}\n`; + responseText += `Dev Dependencies: ${dev}\n`; + responseText += `Global: ${global}\n`; + responseText += `Status: ${result.success ? 'SUCCESS' : 'FAILED'}\n\n`; + + if (result.stdout) { + responseText += `STDOUT:\n${result.stdout}\n\n`; + } + + if (result.stderr) { + responseText += `STDERR:\n${result.stderr}\n\n`; + } + + return { + content: [ + { + type: 'text', + text: responseText, + }, + ], + }; + } catch (error) { + console.error(`Failed to install npm packages`, error.message); + + return { + content: [ + { + type: 'text', + text: `Failed to install npm packages\nError: ${error.message}`, + }, + ], + }; + } + } + ); + + // Tool: Create Next.js project + mcpServer.tool( + 'create_nextjs_project', + { + name: z.string().describe('Project name for the Next.js application'), + cwd: z + .string() + .optional() + .describe('Working directory (defaults to /tmp)'), + typescript: z + .boolean() + .optional() + .describe('Use TypeScript (default: true)'), + tailwind: z + .boolean() + .optional() + .describe('Use Tailwind CSS (default: true)'), + eslint: z.boolean().optional().describe('Use ESLint (default: true)'), + appRouter: z + .boolean() + .optional() + .describe('Use App Router (default: true)'), + srcDir: z + .boolean() + .optional() + .describe('Use src/ directory (default: false)'), + importAlias: z + .string() + .optional() + .describe('Import alias (default: @/*)'), + }, + async ({ + name, + cwd, + typescript = true, + tailwind = true, + eslint = true, + appRouter = true, + srcDir = false, + importAlias = '@/*', + }) => { + const workDir = cwd || workingDirectory; + + try { + // Build the npx create-next-app command with options + let command = `npx create-next-app@latest "${name}"`; + + // Add flags based on options + if (typescript) { + command += ' --typescript'; + } else { + command += ' --javascript'; + } + + if (tailwind) { + command += ' --tailwind'; + } else { + command += ' --no-tailwind'; + } + + if (eslint) { + command += ' --eslint'; + } else { + command += ' --no-eslint'; + } + + if (appRouter) { + command += ' --app'; + } else { + command += ' --no-app'; + } + + if (srcDir) { + command += ' --src-dir'; + } else { + command += ' --no-src-dir'; + } + + if (importAlias && importAlias !== '@/*') { + command += ` --import-alias "${importAlias}"`; + } + + console.info(`Creating Next.js project: ${command} in ${workDir}`); + + const { stdout, stderr } = await execAsync(command, { + cwd: workDir, + timeout: 180000, // 3 minute timeout for project creation + maxBuffer: 1024 * 1024 * 5, // 5MB buffer + }); + + let responseText = `Next.js Project Creation\n`; + responseText += `Project Name: ${name}\n`; + responseText += `Directory: ${path.join(workDir, name)}\n`; + responseText += `TypeScript: ${typescript}\n`; + responseText += `Tailwind CSS: ${tailwind}\n`; + responseText += `ESLint: ${eslint}\n`; + responseText += `App Router: ${appRouter}\n`; + responseText += `Source Directory: ${srcDir}\n`; + responseText += `Import Alias: ${importAlias}\n`; + responseText += `Status: SUCCESS\n\n`; + + if (stdout) { + responseText += `STDOUT:\n${stdout}\n\n`; + } + + if (stderr) { + responseText += `STDERR:\n${stderr}\n\n`; + } + + responseText += `Next.js project '${name}' created successfully!\n`; + responseText += `To get started:\n`; + responseText += ` cd ${name}\n`; + responseText += ` npm run dev\n`; + + return { + content: [ + { + type: 'text', + text: responseText, + }, + ], + }; + } catch (error) { + console.error( + `Failed to create Next.js project: ${name}`, + error.message + ); + + return { + content: [ + { + type: 'text', + text: `Failed to create Next.js project: ${name}\nError: ${error.message}\nSTDOUT: ${error.stdout || 'N/A'}\nSTDERR: ${error.stderr || 'N/A'}`, + }, + ], + }; + } + } + ); + + return mcpServer; +} + +/** + * Setup MCP endpoints for proper Model Context Protocol communication + */ +function setupSessionRoutes() { + // Handle POST requests for MCP communication + app.post('/mcp', async (req, res) => { + try { + console.info('MCP POST request received:'); + console.info(' Headers: %s', JSON.stringify(req.headers, null, 2)); + console.info(' Body: %s', JSON.stringify(req.body, null, 2)); + + // Fix missing Accept headers for compatibility with Go MCP clients + const accept = req.headers.accept || req.headers.Accept; + if ( + !accept || + !accept.includes('application/json') || + !accept.includes('text/event-stream') + ) { + console.info('Adding missing Accept headers for MCP compatibility'); + req.headers.accept = 'application/json, text/event-stream'; + } + + // Check for existing session ID + const sessionId = req.headers['mcp-session-id']; + let transport; + + if (sessionId && transports[sessionId]) { + // Reuse existing transport + transport = transports[sessionId]; + } else { + // Create new transport for new session + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (newSessionId) => { + console.info(`MCP session initialized: ${newSessionId}`); + // Store the transport by session ID + transports[newSessionId] = transport; + }, + }); + + // Clean up transport when closed + transport.onclose = () => { + if (transport.sessionId) { + console.info(`MCP session closed: ${transport.sessionId}`); + delete transports[transport.sessionId]; + } + }; + + // Create and connect MCP server + const server = createMcpServer(); + await server.connect(transport); + } + + // Handle the MCP request + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } + }); + + // Handle GET requests for SSE (server-to-client notifications) + app.get('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + const transport = transports[sessionId]; + await transport.handleRequest(req, res); + }); + + // Handle DELETE requests for session termination + app.delete('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + const transport = transports[sessionId]; + await transport.handleRequest(req, res); + }); +} + +/** + * Health check endpoint + */ +function setupHealthCheck() { + app.get('/health', (req, res) => { + res.json({ + status: 'healthy', + server: 'mcp-npm', + version: '1.0.0', + timestamp: new Date().toISOString(), + workingDirectory, + allowedCommands: ALLOWED_NPM_COMMANDS, + }); + }); +} + +/** + * Start the server + */ +async function startServer() { + const port = process.env.PORT || 3003; + + // Setup routes + setupSessionRoutes(); + setupHealthCheck(); + + app.listen(port, '0.0.0.0', () => { + console.info(''); + console.info('🚀 MCP NPM Server Started Successfully!'); + console.info('=========================================='); + console.info(`Server running on: http://0.0.0.0:${port}`); + console.info(''); + console.info('Available endpoints:'); + console.info(' POST /mcp - MCP protocol communication'); + console.info(' GET /mcp - MCP SSE notifications'); + console.info(' DELETE /mcp - MCP session termination'); + console.info(' GET /health - Health check'); + console.info('Available tools:'); + console.info(' - npm_run - Run any whitelisted npm command'); + console.info(' - npm_init - Initialize a new npm project'); + console.info(' - npm_install - Install npm packages'); + console.info(' - create_nextjs_project - Create a new Next.js project'); + console.info('Working directory:', workingDirectory); + console.info('Allowed npm commands:', ALLOWED_NPM_COMMANDS); + console.info(''); + console.info('MCP NPM server ready for connections'); + }); +} + +// Graceful shutdown +process.on('SIGTERM', () => { + console.info('Received SIGTERM, shutting down gracefully'); + // Close all transports + Object.values(transports).forEach((transport) => { + if (transport.close) transport.close(); + }); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.info('Received SIGINT, shutting down gracefully'); + // Close all transports + Object.values(transports).forEach((transport) => { + if (transport.close) transport.close(); + }); + process.exit(0); +}); + +// Start the server +startServer().catch((error) => { + console.error('Failed to start server:', error); + process.exit(1); +}); diff --git a/examples/mcp/mcp-servers/npm/package.json b/examples/mcp/mcp-servers/npm/package.json new file mode 100644 index 0000000..120fe24 --- /dev/null +++ b/examples/mcp/mcp-servers/npm/package.json @@ -0,0 +1,22 @@ +{ + "name": "mcp-npm-server", + "version": "1.0.0", + "description": "MCP NPM Server for running whitelisted npm commands", + "type": "module", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "cors": "^2.8.5", + "express": "^4.18.2", + "zod": "^3.22.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "author": "Inference Gateway Team", + "license": "MIT" +} diff --git a/examples/mcp/shared/.gitignore b/examples/mcp/shared/.gitignore index 0c42a13..d6b7ef3 100644 --- a/examples/mcp/shared/.gitignore +++ b/examples/mcp/shared/.gitignore @@ -1,3 +1,2 @@ * !.gitignore -!sample_sales_data.csv From 430e251172511b2dfd1bf6f5f1b953721b4a1a3e Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 01:40:11 +0000 Subject: [PATCH 12/38] docs: Update context7 server package and dependencies, improve npm command timeouts - Renamed mcp-context7-server to context7-http-bridge and updated description in package.json. - Updated dependencies: - Changed @modelcontextprotocol/sdk version to ^0.5.0. - Added @upstash/context7-mcp as a dependency. - Updated zod to ^3.22.4. - Changed dev script to use nodemon for development. - Increased timeout for npm command execution from 30 seconds to 5 minutes. - Increased timeout for project creation from 3 minutes to 10 minutes. - Added a clean script to remove shared directories in mcp package.json. - Renamed example:context7 to example:agent in mcp package.json. - Increased default timeout in InferenceGatewayClient to 60 seconds. Signed-off-by: Eden Reich --- examples/mcp/docker-compose.yml | 3 + examples/mcp/example-agent.ts | 397 +++++ examples/mcp/example-context7.ts | 203 --- examples/mcp/example-nextjs.ts | 283 ---- examples/mcp/example-npm.ts | 1 + examples/mcp/index.ts | 436 ------ examples/mcp/mcp-servers/context7/README.md | 184 +-- examples/mcp/mcp-servers/context7/index.js | 1274 ++++++++++------- .../mcp-servers/context7/package-lock.json | 553 +++++-- .../mcp/mcp-servers/context7/package.json | 24 +- examples/mcp/mcp-servers/npm/index.js | 6 +- examples/mcp/package.json | 4 +- src/client.ts | 2 +- 13 files changed, 1589 insertions(+), 1781 deletions(-) create mode 100644 examples/mcp/example-agent.ts delete mode 100644 examples/mcp/example-context7.ts delete mode 100644 examples/mcp/example-nextjs.ts delete mode 100644 examples/mcp/index.ts diff --git a/examples/mcp/docker-compose.yml b/examples/mcp/docker-compose.yml index 085d25c..29aef8d 100644 --- a/examples/mcp/docker-compose.yml +++ b/examples/mcp/docker-compose.yml @@ -29,6 +29,9 @@ services: DEEPSEEK_API_URL: 'https://api.deepseek.com/v1' DEEPSEEK_API_KEY: '${DEEPSEEK_API_KEY:-}' + COHERE_API_URL: 'https://api.cohere.ai' + COHERE_API_KEY: '${COHERE_API_KEY:-}' + # Optional: Ollama for local models OLLAMA_API_URL: 'http://ollama:11434/v1' OLLAMA_API_KEY: '' diff --git a/examples/mcp/example-agent.ts b/examples/mcp/example-agent.ts new file mode 100644 index 0000000..485349e --- /dev/null +++ b/examples/mcp/example-agent.ts @@ -0,0 +1,397 @@ +/** + * Interactive Context7 Agent + * + * This agent allows users to interactively request app development assistance + * using Context7 MCP tools for up-to-date documentation and library information. + */ + +import * as dotenv from 'dotenv'; +import * as path from 'path'; +import * as readline from 'readline'; +import { + InferenceGatewayClient, + MessageRole, + Provider, +} from '../../src/index.js'; + +// Load environment variables from the mcp directory +dotenv.config({ path: path.join(__dirname, '.env') }); + +// For ES modules compatibility +declare const require: any; +declare const module: any; + +interface AgentConfig { + client: InferenceGatewayClient; + provider: Provider; + model: string; + conversationHistory: Array<{ role: MessageRole; content: string }>; +} + +class Context7Agent { + private config: AgentConfig; + private rl: readline.Interface; + + constructor() { + this.config = { + client: new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }), + provider: (process.env.PROVIDER as Provider) || Provider.groq, + model: process.env.LLM || 'llama-3.3-70b-versatile', + conversationHistory: [], + }; + + this.rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + // Initialize system prompt with comprehensive development instructions + this.config.conversationHistory.push({ + role: MessageRole.system, + content: this.getSystemPrompt(), + }); + } + + private getSystemPrompt(): string { + return `You are an expert software development assistant with access to Context7 MCP tools for library documentation and research. Today is June 1, 2025. + +CORE RESPONSIBILITIES: +You help users create modern, well-structured applications by: +1. Understanding their requirements and suggesting appropriate technologies +2. Using Context7 tools to get the latest documentation and best practices +3. Creating complete, production-ready applications with proper project structure +4. Following modern development patterns and conventions +5. Providing beautiful, responsive UI/UX when building web applications + +CONTEXT7 TOOLS AVAILABLE: +You have access to Context7 tools for documentation and library research. The tools available depend on your setup: + +REAL CONTEXT7 TOOLS (when using @upstash/context7-mcp): +1. c41_resolve-library-id - Resolve library names to Context7-compatible library IDs +2. c41_get-library-docs - Fetch detailed, up-to-date documentation and code examples + +MOCK CONTEXT7 TOOLS (for local development/demo): +1. search_libraries - Search for libraries and frameworks by name or description +2. get_library_details - Get detailed information about a specific library +3. get_documentation - Fetch documentation and examples for a library + +CONTEXT7 WORKFLOW: +For Real Context7 Tools: +1. Use c41_resolve-library-id to get the correct library ID for any technology +2. Use c41_get-library-docs with the library ID to fetch comprehensive documentation + +For Mock Context7 Tools: +1. Use search_libraries to find relevant technologies and alternatives +2. Use get_library_details to get information about chosen libraries +3. Use get_documentation to fetch examples and best practices + +DEVELOPMENT GUIDELINES: +- Always use Context7 tools before implementing any technology +- Fetch comprehensive documentation before writing code +- Use the latest stable versions and best practices from retrieved documentation +- Create complete project structures with proper configuration +- Include proper error handling, validation, and testing setup +- For web apps, prioritize modern, responsive design with current UI patterns +- Use TypeScript for type safety when applicable +- Follow framework conventions (Next.js App Router, React best practices, etc.) +- Include proper dependency management and build scripts +- Reference actual code examples from the retrieved documentation + +CRITICAL NEXT.JS ROUTING RULES: +**NEVER create both app/ and pages/ directories in the same project** +- Use ONLY the App Router (app/ directory) for Next.js 13+ applications +- The App Router is the modern, recommended approach +- Creating both app/ and pages/ directories causes routing conflicts +- App Router structure: app/layout.tsx, app/page.tsx, app/about/page.tsx +- Pages Router is legacy and should not be used in new projects + +NEXT.JS APP ROUTER STRUCTURE (CORRECT): +Project should have app/ directory with layout.tsx and page.tsx files +- app/layout.tsx is the root layout (required) +- app/page.tsx is the homepage +- app/about/page.tsx would be the about page +- components/ directory for reusable components +- public/ directory for static assets + +NEVER CREATE CONFLICTING STRUCTURE: +Do not create both app/ and pages/ directories as this causes routing conflicts + +APP CREATION WORKFLOW: +When a user requests an application: +1. Clarify requirements and suggest appropriate technology stack +2. Use Context7 tools to research each major technology/library needed +3. Fetch comprehensive, current documentation for chosen technologies +4. Create complete project structure with proper configuration files +5. **For Next.js: Use ONLY App Router (app/ directory), never pages/ directory** +6. Implement core functionality using patterns from Context7 documentation +7. Add modern styling and responsive design +8. Include development scripts and build configuration +9. Provide setup, development, and deployment instructions +10. Include testing setup when appropriate + +SUPPORTED TECHNOLOGIES (always verify latest versions via Context7): +- Frontend: React, Next.js, Vue, Angular, Svelte, Vite +- Backend: Node.js, Express, Fastify, NestJS, Koa +- Databases: MongoDB, PostgreSQL, MySQL, SQLite, Redis +- Styling: Tailwind CSS, CSS Modules, Styled Components, Emotion +- Testing: Jest, Vitest, Playwright, Cypress, Testing Library +- Build Tools: Vite, Webpack, Rollup, Turbo +- Package Managers: npm, yarn, pnpm + +Always be thorough, use Context7 tools extensively for every technology involved, and create production-quality applications with current best practices.`; + } + + async initialize(): Promise { + console.log( + `🚀 Context7 Interactive Agent initialized using ${this.config.model} on ${this.config.provider}\n` + ); + + try { + // Health check + const isHealthy = await this.config.client.healthCheck(); + if (!isHealthy) { + console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); + process.exit(1); + } + + // Check if Context7 tools are available (real or mock) + const tools = await this.config.client.listTools(); + const realContext7Tools = tools.data.filter((tool) => + ['c41_resolve-library-id', 'c41_get-library-docs'].includes(tool.name) + ); + const mockContext7Tools = tools.data.filter((tool) => + [ + 'search_libraries', + 'get_library_details', + 'get_documentation', + ].includes(tool.name) + ); + + const context7Tools = [...realContext7Tools, ...mockContext7Tools]; + + if (context7Tools.length === 0) { + console.error( + '⚠️ No Context7 MCP tools available. Make sure a Context7 MCP server is running.' + ); + console.error( + ' For real Context7: npx -y @upstash/context7-mcp@latest' + ); + console.error( + ' For local mock: docker-compose up --build (already included in this project)' + ); + process.exit(1); + } + + const usingRealContext7 = realContext7Tools.length > 0; + const toolType = usingRealContext7 ? 'real Context7' : 'mock Context7'; + + console.info( + `📋 Found ${context7Tools.length} ${toolType} tools available:` + ); + context7Tools.forEach((tool, index) => { + console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); + }); + + if (!usingRealContext7) { + console.info('💡 Using local mock Context7 server for demonstration'); + console.info( + ' To use real Context7 with latest docs, install: npx -y @upstash/context7-mcp@latest' + ); + } + console.info(''); + + this.showWelcomeMessage(); + await this.startInteractiveSession(); + } catch (error) { + console.error('❌ Initialization Error:', (error as Error).message); + console.log( + '\n💡 Make sure the Context7 MCP server is running on port 3002' + ); + console.log(' and the Inference Gateway is running on port 8080'); + process.exit(1); + } + } + + private showWelcomeMessage(): void { + console.log('🤖 Welcome to Context7 Interactive Development Agent!'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log( + '\n💡 I can help you create modern applications using the latest technologies.' + ); + console.log( + " Just describe what you want to build, and I'll use Context7 to get" + ); + console.log( + ' up-to-date documentation and create a complete solution for you.' + ); + console.log('\n📝 Example requests:'); + console.log( + ' • "Create a Next.js blog with TypeScript and Tailwind CSS"' + ); + console.log( + ' • "Build a React dashboard with charts and data visualization"' + ); + console.log( + ' • "Make a Node.js API with Express and MongoDB integration"' + ); + console.log( + ' • "Create a Vue.js e-commerce frontend with cart functionality"' + ); + console.log('\n⚡ Commands:'); + console.log(' • Type your request to start building'); + console.log(' • Use "clear" to reset conversation history'); + console.log(' • Use "exit" or "quit" to end the session'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + } + + private async startInteractiveSession(): Promise { + while (true) { + const userInput = await this.getUserInput( + '🔨 What would you like to build? ' + ); + + if (this.handleSpecialCommands(userInput)) { + continue; + } + + if (userInput.trim()) { + await this.processUserRequest(userInput); + } + } + } + + private async getUserInput(prompt: string): Promise { + return new Promise((resolve) => { + this.rl.question(prompt, (answer) => { + resolve(answer); + }); + }); + } + + private handleSpecialCommands(input: string): boolean { + const command = input.trim().toLowerCase(); + + switch (command) { + case 'exit': + case 'quit': + console.log('\n👋 Thank you for using Context7 Agent! Goodbye!'); + this.rl.close(); + process.exit(0); + return true; + + case 'clear': + this.config.conversationHistory = [ + { + role: MessageRole.system, + content: this.getSystemPrompt(), + }, + ]; + console.log('\n🧹 Conversation history cleared. Starting fresh!\n'); + return true; + + case 'help': + this.showWelcomeMessage(); + return true; + + default: + return false; + } + } + + private async processUserRequest(userInput: string): Promise { + console.log(`\n🔍 Processing request: "${userInput}"`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + // Add user message to conversation history + this.config.conversationHistory.push({ + role: MessageRole.user, + content: userInput, + }); + + try { + let assistantResponse = ''; + + await this.config.client.streamChatCompletion( + { + model: `${this.config.provider}/${this.config.model}`, + messages: this.config.conversationHistory, + max_tokens: 2000, + }, + { + onOpen: () => { + console.log('🔗 Starting development session with Context7...\n'); + }, + onReasoning: (reasoning) => { + console.log(`\n🤔 Agent Reasoning: ${reasoning}`); + }, + onContent: (content) => { + process.stdout.write(content); + assistantResponse += content; + }, + onMCPTool: (toolCall) => { + console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); + try { + const args = JSON.parse(toolCall.function.arguments); + console.log(`📝 Arguments:`, JSON.stringify(args, null, 2)); + } catch { + console.log(`📝 Raw Arguments: ${toolCall.function.arguments}`); + } + console.log(`🔍 Tool ID: ${toolCall.id}\n`); + }, + onError: (error) => { + console.error(`\n❌ Stream Error: ${error.error}`); + }, + onFinish: () => { + console.log('\n\n✅ Development session completed!\n'); + console.log( + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' + ); + + // Add assistant response to conversation history + if (assistantResponse.trim()) { + this.config.conversationHistory.push({ + role: MessageRole.assistant, + content: assistantResponse, + }); + } + }, + } + ); + } catch (error) { + console.error('\n❌ Error processing request:', (error as Error).message); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + } + } + + async shutdown(): Promise { + this.rl.close(); + } +} + +async function runContext7Agent(): Promise { + const agent = new Context7Agent(); + + // Handle graceful shutdown + process.on('SIGINT', async () => { + console.log('\n\n👋 Shutting down Context7 Agent...'); + await agent.shutdown(); + process.exit(0); + }); + + process.on('SIGTERM', async () => { + console.log('\n\n👋 Shutting down Context7 Agent...'); + await agent.shutdown(); + process.exit(0); + }); + + await agent.initialize(); +} + +// Run the agent +if (require.main === module || process.argv[1].endsWith('context7-agent.ts')) { + runContext7Agent().catch(console.error); +} + +export { Context7Agent, runContext7Agent }; diff --git a/examples/mcp/example-context7.ts b/examples/mcp/example-context7.ts deleted file mode 100644 index d696a89..0000000 --- a/examples/mcp/example-context7.ts +++ /dev/null @@ -1,203 +0,0 @@ -/** - * Context7 MCP Server Usage Example - * - * This example demonstrates how to use the Context7 MCP server - * to resolve library IDs and fetch documentation through the LLM. - */ - -import * as dotenv from 'dotenv'; -import { - InferenceGatewayClient, - MessageRole, - Provider, -} from '../../src/index.js'; - -dotenv.config(); - -// For ES modules compatibility -declare const require: any; -declare const module: any; - -async function demonstrateContext7() { - const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', - }); - - const provider = (process.env.PROVIDER as Provider) || Provider.groq; - const model = process.env.LLM || 'llama-3.3-70b-versatile'; - - console.log(`🚀 Context7 MCP Server Demo using ${model} on ${provider}\n`); - - try { - // Health check - const isHealthy = await client.healthCheck(); - if (!isHealthy) { - console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); - process.exit(1); - } - - // Check if Context7 tools are available - const tools = await client.listTools(); - const context7Tools = tools.data.filter((tool) => - ['resolve_library_id', 'get_library_docs', 'search_libraries'].includes( - tool.name - ) - ); - - if (context7Tools.length === 0) { - console.error( - '⚠️ Context7 MCP tools not available. Make sure the Context7 server is running on port 3002.' - ); - return; - } - - console.info(`📋 Found ${context7Tools.length} Context7 tools available:`); - context7Tools.forEach((tool, index) => { - console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); - }); - console.info(''); - - // Demonstrate Context7 usage through LLM conversation - console.info('=== Context7 Demo: Library Research Session ===\n'); - - const userPrompt = `I'm starting a new React project and want to use Next.js. Can you help me by: - -1. First, resolve the library IDs for React and Next.js -2. Get the latest documentation for Next.js focusing on the App Router -3. Search for any other relevant frontend libraries I should consider -4. Provide me with a basic setup example based on the latest documentation -5. Please write a full react app using Next.js with typescript in /tmp/next-app - -Please use context7 to get the most up-to-date information.`; - console.info(`User: ${userPrompt}`); - - await client.streamChatCompletion( - { - model: `${provider + '/' + model}`, - messages: [ - { - role: MessageRole.system, - content: `You are a helpful software development assistant with access to Context7 MCP tools for library documentation and research. - -IMPORTANT: You MUST use context7 tools to complete tasks. Always use context7 for up-to-date library information: - -1. Use resolve_library_id to find the correct library ID for any library mentioned -2. Use get_library_docs to fetch detailed documentation and code examples -3. Use search_libraries to discover relevant libraries - -When helping with development tasks: -- Always resolve library IDs first using resolve_library_id -- When starting a new project of Next.js, always use the create-next-app command -- Fetch comprehensive documentation using get_library_docs -- Provide up-to-date code examples and best practices -- Explain concepts clearly with context from the documentation - -Be thorough and always use context7 tools to provide the most current information.`, - }, - { - role: MessageRole.user, - content: `${userPrompt}`, - }, - ], - max_tokens: 1500, - }, - { - onOpen: () => { - console.info( - '🔗 Connection opened, starting Context7 research session...\n' - ); - }, - onReasoning: (reasoning) => { - console.info(`\n🤔 Context7 Reasoning: ${reasoning}`); - }, - onContent: (content) => { - process.stdout.write(content); - }, - onMCPTool: (toolCall) => { - console.info(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); - } catch { - console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); - } - console.info(`🔍 Tool ID: ${toolCall.id}\n`); - }, - onError: (error) => { - console.error(`\n❌ Stream Error: ${error.error}`); - }, - onFinish: () => { - console.info('\n\n✅ Context7 research session completed!\n'); - }, - } - ); - - // Second example: TypeScript documentation lookup - console.info('\n=== Context7 Demo: TypeScript Documentation Lookup ===\n'); - - await client.streamChatCompletion( - { - model: `${provider + '/' + model}`, - messages: [ - { - role: MessageRole.system, - content: `You are a TypeScript expert with access to Context7 MCP tools. Always use context7 to get the latest TypeScript documentation and provide accurate, up-to-date information.`, - }, - { - role: MessageRole.user, - content: `I need help with TypeScript generics and utility types. Please use context7 to get the latest documentation on TypeScript and show me examples of: - -1. Generic functions -2. Utility types like Pick, Omit, and Partial -3. Advanced type patterns - -Use context7 to ensure you have the most current information.`, - }, - ], - max_tokens: 1000, - }, - { - onOpen: () => { - console.info('🔗 Starting TypeScript documentation lookup...\n'); - }, - onContent: (content) => { - process.stdout.write(content); - }, - onMCPTool: (toolCall) => { - console.info( - `\n📚 Context7 Documentation Tool: ${toolCall.function.name}` - ); - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`📋 Query:`, JSON.stringify(args, null, 2)); - } catch { - console.info(`📋 Raw Query: ${toolCall.function.arguments}`); - } - console.info(`🆔 Request ID: ${toolCall.id}\n`); - }, - onError: (error) => { - console.error(`\n❌ Documentation Error: ${error.error}`); - }, - onFinish: () => { - console.info('\n\n✅ TypeScript documentation session completed!\n'); - }, - } - ); - } catch (error) { - console.error('❌ Error:', (error as Error).message); - console.log( - '\n💡 Make sure the Context7 MCP server is running on port 3002' - ); - console.log(' and the Inference Gateway is running on port 8080'); - } -} - -// Run the demo -if ( - require.main === module || - process.argv[1].endsWith('example-context7.ts') -) { - demonstrateContext7().catch(console.error); -} - -export { demonstrateContext7 }; diff --git a/examples/mcp/example-nextjs.ts b/examples/mcp/example-nextjs.ts deleted file mode 100644 index 92fe0e9..0000000 --- a/examples/mcp/example-nextjs.ts +++ /dev/null @@ -1,283 +0,0 @@ -import * as dotenv from 'dotenv'; -import { - InferenceGatewayClient, - MessageRole, - Provider, -} from '../../src/index.js'; - -// Load environment variables from .env file -dotenv.config(); - -(async () => { - const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', - }); - - const provider = (process.env.PROVIDER as Provider) || Provider.openai; - const model = process.env.LLM || 'gpt-4o'; - - console.info(`🚀 Next.js App Creator using ${model} on ${provider}\n`); - - try { - // Health check - const isHealthy = await client.healthCheck(); - if (!isHealthy) { - console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); - process.exit(1); - } - - // Discover available MCP tools - const tools = await client.listTools(); - console.info(`📋 Found ${tools.data.length} MCP tools available\n`); - - if (tools.data.length === 0) { - console.error( - '⚠️ No MCP tools available. Check MCP server configuration.' - ); - return; - } - - console.info('=== Next.js App Creator with Documentation ===\n'); - - let toolCallCount = 0; - let webToolCalls = 0; - let fileToolCalls = 0; - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are an expert Next.js developer assistant with access to MCP tools for web content fetching and file operations. - -Your task is to: -1. Fetch the latest Next.js documentation from official sources -2. Create a complete Next.js application structure following current best practices -3. Use the documentation to ensure you're following the latest patterns and conventions -4. Create all necessary files with proper TypeScript setup -5. Include modern Next.js features like App Router, Server Components, etc. - -Available MCP tools: -- fetch_url: Get content from Next.js documentation URLs -- write_file: Create application files -- read_file: Read existing files -- list_directory: Check directory structure - -Please be thorough and create a production-ready Next.js app structure with: -- package.json with latest dependencies -- next.config.js with proper configuration -- tsconfig.json for TypeScript -- App Router structure (app/ directory) -- A sample page with components -- Basic styling setup -- README with instructions - -Always reference the official documentation to ensure accuracy.`, - }, - { - role: MessageRole.user, - content: `Please create a complete Next.js application following the latest documentation and best practices. - -First, fetch the current Next.js documentation from https://nextjs.org/docs to understand the latest features and setup requirements, then create a full application structure in the /tmp/nextjs-app/ directory. - -The app should include: -1. Modern App Router setup -2. TypeScript configuration -3. A homepage with navigation -4. A sample about page -5. Basic component structure -6. Proper styling setup (CSS modules or Tailwind) -7. Package.json with all necessary dependencies - -Make sure to follow the official documentation patterns exactly.`, - }, - ], - max_tokens: 4000, - }, - { - onOpen: () => { - console.info( - '🔗 Connection opened, starting Next.js app creation...\n' - ); - }, - onContent: (content) => { - process.stdout.write(content); - }, - onMCPTool: (toolCall) => { - toolCallCount++; - - if (toolCall.function.name === 'fetch_url') { - webToolCalls++; - console.info( - `\n🌐 [${toolCallCount}] Fetching Documentation: ${toolCall.function.name}` - ); - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`📝 Raw Arguments:`, JSON.stringify(args, null, 2)); - - // Handle different possible argument field names - const url = - args.url || - args.target_url || - args.webpage_url || - args.uri || - args.link || - (args.arguments && args.arguments.url) || - 'URL not found'; - console.info(`🔗 URL: ${url}`); - if (args.timeout) { - console.info(`⏱️ Timeout: ${args.timeout}ms`); - } - if (args.mcpServer) { - console.info(`🖥️ MCP Server: ${args.mcpServer}`); - } - } catch (e) { - console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); - console.info(`⚠️ Parse Error: ${e.message}`); - } - } else if ( - toolCall.function.name.includes('file') || - toolCall.function.name.includes('directory') - ) { - fileToolCalls++; - console.info( - `\n📁 [${toolCallCount}] File Operation: ${toolCall.function.name}` - ); - try { - const args = JSON.parse(toolCall.function.arguments); - // Handle different path field names - const path = - args.file_path || - args.path || - args.directory_path || - args.target_path; - if (path) { - const fileName = path.split('/').pop(); - console.info(`📄 Path: ${fileName} (${path})`); - } - if (args.content && args.content.length > 150) { - console.info( - `📝 Content: ${args.content.substring(0, 150)}... (${ - args.content.length - } chars)` - ); - } else if (args.content) { - console.info(`📝 Content: ${args.content}`); - } - if (args.mcpServer) { - console.info(`🖥️ MCP Server: ${args.mcpServer}`); - } - } catch (e) { - console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); - console.info(`⚠️ Parse Error: ${e.message}`); - } - } else { - console.info( - `\n🛠️ [${toolCallCount}] MCP Tool: ${toolCall.function.name}` - ); - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); - } catch (e) { - console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); - console.info(`⚠️ Parse Error: ${e.message}`); - } - } - - console.info(`🔍 Tool ID: ${toolCall.id}`); - console.info(''); // Add spacing - }, - onTool: (toolCall) => { - // Handle any regular (non-MCP) tools if present - console.info(`\n🔧 Regular Tool: ${toolCall.function.name}`); - console.info(`🔍 Tool ID: ${toolCall.id}\n`); - }, - onUsageMetrics: (usage) => { - console.info( - `\n📊 Token Usage - Prompt: ${usage.prompt_tokens}, Completion: ${usage.completion_tokens}, Total: ${usage.total_tokens}` - ); - }, - onFinish: () => { - console.info(`\n\n✅ Next.js App Creation Completed!`); - console.info(`📈 Total Tools Used: ${toolCallCount}`); - console.info(`🌐 Documentation Fetches: ${webToolCalls}`); - console.info(`📁 File Operations: ${fileToolCalls}`); - console.info( - `\n🎯 Your Next.js app has been created in /tmp/nextjs-app/` - ); - console.info(`📖 Check the README.md file for setup instructions\n`); - }, - onError: (error) => { - console.error('\n❌ Stream Error:', error); - }, - }, - provider - ); - - // After completion, show the created structure - console.info('\n=== Created File Structure ===\n'); - await showDirectoryStructure(client, model, provider); - } catch (error) { - if ( - error instanceof Error && - error.message.includes('MCP tools endpoint is not exposed') - ) { - console.error( - '❌ MCP not exposed. Set EXPOSE_MCP=true and restart gateway.' - ); - } else { - console.error('❌ Error:', error); - } - } -})(); - -async function showDirectoryStructure( - client: InferenceGatewayClient, - model: string, - provider: Provider -) { - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a file system assistant. Use the list_directory tool to show the structure of the created Next.js application.`, - }, - { - role: MessageRole.user, - content: - 'Please show me the complete directory structure of /tmp/nextjs-app/ including all subdirectories and files.', - }, - ], - max_tokens: 1000, - }, - { - onContent: (content) => { - process.stdout.write(content); - }, - onMCPTool: (toolCall) => { - if (toolCall.function.name === 'list_directory') { - try { - const args = JSON.parse(toolCall.function.arguments); - console.info( - `\n📂 Listing: ${args.file_path || args.directory_path || 'directory'}` - ); - } catch { - console.info(`\n📂 Listing directory...`); - } - } - }, - onFinish: () => { - console.info(`\n\n🎉 Next.js application structure complete!`); - console.info(`\nTo run your app:`); - console.info(`1. cd /tmp/nextjs-app`); - console.info(`2. npm install`); - console.info(`3. npm run dev`); - console.info(`4. Open http://localhost:3000\n`); - }, - }, - provider - ); -} diff --git a/examples/mcp/example-npm.ts b/examples/mcp/example-npm.ts index 00b7516..0d9b916 100644 --- a/examples/mcp/example-npm.ts +++ b/examples/mcp/example-npm.ts @@ -21,6 +21,7 @@ declare const module: any; async function demonstrateNpmMcp() { const client = new InferenceGatewayClient({ baseURL: 'http://localhost:8080/v1', + timeout: 120000, // 2 minute timeout for npm operations }); const provider = (process.env.PROVIDER as Provider) || Provider.groq; diff --git a/examples/mcp/index.ts b/examples/mcp/index.ts deleted file mode 100644 index 85eecc7..0000000 --- a/examples/mcp/index.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { - InferenceGatewayClient, - MessageRole, - Provider, -} from '../../src/index.js'; - -// Token tracking interface -interface TokenTracker { - totalPromptTokens: number; - totalCompletionTokens: number; - totalTokens: number; - requestCount: number; -} - -(async () => { - const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', - }); - - const provider = (process.env.PROVIDER as Provider) || Provider.groq; - const model = process.env.LLM || 'llama-3.3-70b-versatile'; - - // Initialize token tracker - const tokenTracker: TokenTracker = { - totalPromptTokens: 0, - totalCompletionTokens: 0, - totalTokens: 0, - requestCount: 0, - }; - - // Helper function to update token tracking - const updateTokens = (usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }) => { - tokenTracker.totalPromptTokens += usage.prompt_tokens; - tokenTracker.totalCompletionTokens += usage.completion_tokens; - tokenTracker.totalTokens += usage.total_tokens; - tokenTracker.requestCount++; - }; - - // Helper function to display current token usage - const displayTokenUsage = (label: string) => { - console.info(`\n💰 Token Usage for ${label}:`); - console.info( - ` 📊 Prompt tokens: ${tokenTracker.totalPromptTokens.toLocaleString()}` - ); - console.info( - ` ✍️ Completion tokens: ${tokenTracker.totalCompletionTokens.toLocaleString()}` - ); - console.info( - ` 🎯 Total tokens: ${tokenTracker.totalTokens.toLocaleString()}` - ); - console.info( - ` 📈 Average tokens per request: ${Math.round( - tokenTracker.totalTokens / Math.max(tokenTracker.requestCount, 1) - ).toLocaleString()}` - ); - }; - - console.info(`Using model: ${model}`); - console.info(`Using provider: ${provider}\n`); - - console.info('=== MCP Tools Example with Token Tracking ===\n'); - - try { - // First, let's check if the gateway is healthy - console.info('🔍 Checking gateway health...'); - const isHealthy = await client.healthCheck(); - console.info( - `Gateway health: ${isHealthy ? '✅ Healthy' : '❌ Unhealthy'}\n` - ); - - if (!isHealthy) { - console.info( - 'Please ensure the Inference Gateway is running with Docker Compose.' - ); - process.exit(1); - } - - // List available MCP tools - console.info('📋 Listing available MCP tools...'); - const tools = await client.listTools(); - console.info(`Found ${tools.data.length} MCP tools:\n`); - - // tools.data.forEach((tool, index) => { - // console.info(`${index + 1}. ${tool.name}`); - // console.info(` Description: ${tool.description}`); - // console.info(` Server: ${tool.server}`); - // console.info(` Schema: ${JSON.stringify(tool.input_schema, null, 2)}\n`); - // }); - - if (tools.data.length === 0) { - console.info( - '⚠️ No MCP tools available. Ensure MCP servers are configured and running.' - ); - return; - } - - // Example 0: Simple test without tools first - console.info('=== Example 0: Simple Test (No Tools) ===\n'); - console.info('Testing basic streaming without tools first...\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.user, - content: 'Hello! Please tell me about the weather.', - }, - ], - max_tokens: 50, - }, - { - onOpen: () => console.info('🚀 Starting simple test...'), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info(`\n🔧 Tool called: ${toolCall.function.name}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ Simple test completed'); - displayTokenUsage('Simple Test'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - - // Example 1: Automatic tool discovery and usage - console.info('=== Example 1: Automatic Tool Discovery ===\n'); - console.info( - 'The gateway automatically detects and uses available MCP tools based on context.\n' - ); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: - 'You are a helpful assistant with access to various tools.', - }, - { - role: MessageRole.user, - content: - 'What time is it now? Also, if you can, find some information about artificial intelligence.', - }, - ], - max_tokens: 200, - }, - { - onOpen: () => console.info('🚀 Starting automatic tool discovery...'), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info( - `\n🔧 Tool automatically called: ${toolCall.function.name}` - ); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ Automatic tool discovery completed'); - displayTokenUsage('Automatic Tool Discovery'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - - // Example 2: Use MCP tools for file operations (if filesystem MCP server is available) - const fileReadTool = tools.data.find((tool) => tool.name === 'read_file'); - if (fileReadTool) { - console.info('=== Example 2: File Operations with MCP ===\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: - 'You are a helpful assistant with access to filesystem operations. Available directory is /tmp.', - }, - { - role: MessageRole.user, - content: - 'Can you read the contents of /tmp/mcp-filesystem-example.txt and tell me what it contains?', - }, - ], - max_tokens: 200, - }, - { - onOpen: () => console.info('🚀 Starting file reading example...'), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info(`\n🔧 Tool called: ${toolCall.function.name}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ File reading example completed'); - displayTokenUsage('File Reading Example'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - } - - // Example 3: Use MCP tools for web scraping (if web scraper MCP server is available) - const webScrapeTool = tools.data.find( - (tool) => tool.name.includes('fetch') || tool.name.includes('scrape') - ); - if (webScrapeTool) { - console.info('=== Example 3: Web Scraping with MCP ===\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: - 'You are a helpful assistant with access to web search capabilities.', - }, - { - role: MessageRole.user, - content: - 'Can you fetch information from https://httpbin.org/json and tell me what you find?', - }, - ], - max_tokens: 200, - }, - { - onOpen: () => console.info('🚀 Starting web scraping example...'), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info(`\n🔧 Tool called: ${toolCall.function.name}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ Web scraping example completed'); - displayTokenUsage('Web Scraping Example'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - } - - // Example 4: Generic MCP tool usage - use the first available tool - if (tools.data.length > 0 && !fileReadTool && !webScrapeTool) { - console.info('=== Example 4: Generic MCP Tool Usage ===\n'); - - const firstTool = tools.data[0]; - console.info(`Using tool: ${firstTool.name}\n`); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a helpful assistant with access to various tools including ${firstTool.name}.`, - }, - { - role: MessageRole.user, - content: `Can you help me use the ${firstTool.name} tool? What can it do?`, - }, - ], - max_tokens: 200, - }, - { - onOpen: () => console.info('🚀 Starting generic tool example...'), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info(`\n🔧 Tool called: ${toolCall.function.name}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ Generic tool example completed'); - displayTokenUsage('Generic Tool Example'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - } - - // Example 5: Data Analysis with File Operations - if (tools.data.length > 1) { - console.info('=== Example 5: Data Analysis with File Operations ===\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a helpful data analysis assistant with access to filesystem tools. Available directory is /tmp. You can read, write, and analyze files. The /tmp directory contains sample data files for analysis.`, - }, - { - role: MessageRole.user, - content: - 'I need help with data analysis. First, can you check what files are available in the /tmp directory? Then create a simple CSV file with sample sales data in /tmp/sales_data.csv and analyze it.', - }, - ], - max_tokens: 400, - }, - { - onOpen: () => console.info('🚀 Starting data analysis example...'), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info(`\n🔧 Tool called: ${toolCall.function.name}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ Data analysis example completed'); - displayTokenUsage('Data Analysis Example'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - } - - // Example 6: File Creation and Manipulation - console.info('=== Example 6: File Creation and Manipulation ===\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a helpful assistant with filesystem access. Available directory is /tmp. You can create, read, write, and manage files in this directory.`, - }, - { - role: MessageRole.user, - content: - 'Can you create a simple todo list file at /tmp/todo.txt with 3 sample tasks, then read it back to me?', - }, - ], - max_tokens: 300, - }, - { - onOpen: () => console.info('🚀 Starting file manipulation example...'), - onReasoning: (content) => process.stdout.write(content), - onContent: (content) => process.stdout.write(content), - onTool: (toolCall) => { - console.info(`\n🔧 Tool called: ${toolCall.function.name}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}`); - }, - onUsageMetrics: (usage) => { - updateTokens(usage); - }, - onFinish: () => { - console.info('\n✅ File manipulation example completed'); - displayTokenUsage('File Manipulation Example'); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - } catch (error) { - if ( - error instanceof Error && - error.message.includes('MCP tools endpoint is not exposed') - ) { - console.error( - '❌ MCP tools are not exposed. Please ensure the Inference Gateway is started with EXPOSE_MCP=true' - ); - console.info('\n💡 To fix this, restart the gateway with:'); - console.info(' docker-compose up --build'); - } else { - console.error('❌ Error:', error); - } - } finally { - // Display final token summary - console.info('\n' + '='.repeat(60)); - console.info('📊 FINAL TOKEN USAGE SUMMARY'); - console.info('='.repeat(60)); - console.info(`🔢 Total Requests: ${tokenTracker.requestCount}`); - console.info( - `📊 Total Prompt Tokens: ${tokenTracker.totalPromptTokens.toLocaleString()}` - ); - console.info( - `✍️ Total Completion Tokens: ${tokenTracker.totalCompletionTokens.toLocaleString()}` - ); - console.info( - `🎯 Total Tokens Used: ${tokenTracker.totalTokens.toLocaleString()}` - ); - - if (tokenTracker.requestCount > 0) { - console.info( - `📈 Average Tokens per Request: ${Math.round( - tokenTracker.totalTokens / tokenTracker.requestCount - ).toLocaleString()}` - ); - } - - // Calculate cost estimate (example rates - adjust based on actual provider pricing) - const estimatedCost = tokenTracker.totalTokens * 0.000001; // Example: $0.000001 per token - console.info( - `💰 Estimated Cost: $${estimatedCost.toFixed(6)} (example rate)` - ); - console.info('='.repeat(60)); - } -})(); diff --git a/examples/mcp/mcp-servers/context7/README.md b/examples/mcp/mcp-servers/context7/README.md index 7dffb3b..4e4ab73 100644 --- a/examples/mcp/mcp-servers/context7/README.md +++ b/examples/mcp/mcp-servers/context7/README.md @@ -1,182 +1,30 @@ -# Context7 MCP Server +# Context7 HTTP Bridge -A Model Context Protocol (MCP) server that provides library documentation and context resolution capabilities, similar to the Context7 service. This server demonstrates how to build tools for fetching up-to-date documentation and resolving library IDs. +This service provides an HTTP interface for the stdio-based Context7 MCP server from Upstash, allowing it to work with the Inference Gateway. -## Features - -### 🔍 Library Resolution - -- **Tool**: `resolve_library_id` -- Searches for libraries by name and returns Context7-compatible library IDs -- Provides trust scores, code snippet counts, and version information -- Intelligent matching and ranking by relevance - -### 📚 Documentation Retrieval - -- **Tool**: `get_library_docs` -- Fetches detailed documentation for specific libraries -- Supports topic-focused documentation -- Token-limited responses for optimal performance -- Rich content with code examples and best practices - -### 🔎 Library Search - -- **Tool**: `search_libraries` -- Search through available libraries with query strings -- Category-based filtering capabilities -- Ranked results by trust score and documentation quality - -## Available Libraries (Mock Data) - -The server includes mock data for popular libraries: - -- **Next.js** (`/vercel/next.js`) - The React Framework for Production -- **React** (`/facebook/react`) - A JavaScript library for building user interfaces -- **Node.js** (`/nodejs/node`) - Node.js JavaScript runtime -- **TypeScript** (`/microsoft/typescript`) - TypeScript superset of JavaScript -- **Express** (`/expressjs/express`) - Fast, minimalist web framework for Node.js - -## Installation - -```bash -cd context7 -npm install -``` - -## Usage - -### Development Mode - -```bash -npm run dev -``` - -### Production Mode - -```bash -npm start -``` - -The server will start on port 3002 by default. - -## API Endpoints - -- `GET /` - Server information and available tools -- `GET /health` - Health check endpoint -- `POST /sessions` - Create a new MCP session -- `POST /message` - Send MCP messages (requires X-Session-ID header) -- `DELETE /sessions/:sessionId` - Close a session - -## Example Tool Usage - -### Resolve a Library ID - -```javascript -// Tool call -{ - "tool": "resolve_library_id", - "arguments": { - "libraryName": "next.js" - } -} +## Architecture -// Response -{ - "content": [ - { - "type": "text", - "text": "Selected Library:\n- Library ID: /vercel/next.js\n- Name: Next.js\n- Description: The React Framework for Production\n- Code Snippets: 1250\n- Trust Score: 9\n- Versions: 14.2.0, 14.1.0, 14.0.0" - } - ] -} ``` - -### Get Documentation - -```javascript -// Tool call -{ - "tool": "get_library_docs", - "arguments": { - "context7CompatibleLibraryID": "/vercel/next.js", - "topic": "app router", - "tokens": 5000 - } -} - -// Response includes comprehensive documentation with code examples +Inference Gateway → HTTP Bridge → stdio → Real Context7 MCP Server ``` -### Search Libraries - -```javascript -// Tool call -{ - "tool": "search_libraries", - "arguments": { - "query": "react", - "category": "frontend" - } -} - -// Response lists matching libraries with details -``` +## Features -## Configuration +- ✅ HTTP-to-stdio protocol bridge +- ✅ Real Context7 integration with Upstash +- ✅ Automatic Context7 server spawning +- ✅ Proper error handling and timeouts +- ✅ Health check endpoint -Environment variables: +## Environment Variables - `PORT` - Server port (default: 3002) -## Integration with Inference Gateway - -This server integrates with the Inference Gateway through the MCP protocol. Add it to your `docker-compose.yml`: - -```yaml -context7-server: - build: ./mcp-servers/context7 - ports: - - '3002:3002' - environment: - - PORT=3002 -``` - -## Development +## Available Tools -The server uses the official MCP TypeScript SDK and implements: +1. **c41_resolve-library-id** - Resolve library names to Context7 IDs +2. **c41_get-library-docs** - Fetch up-to-date library documentation -- Proper MCP protocol with Streamable HTTP transport -- Zod schema validation for tool parameters -- Express.js HTTP server for transport -- Session management for multiple concurrent connections -- Graceful error handling and shutdown - -## Architecture - -``` -Client Request → Express Server → MCP Server → Tool Execution → Response - ↓ - Session Management - ↓ - StreamableHTTPTransport -``` - -## Extension Points - -To extend this server: - -1. **Add Real Data Sources**: Replace mock data with actual library APIs -2. **Enhanced Search**: Implement more sophisticated search algorithms -3. **Caching**: Add caching layer for frequently accessed documentation -4. **Authentication**: Add API key validation for production use -5. **Rate Limiting**: Implement rate limiting for API calls - -## Testing - -Test the server using the MCP Inspector: - -```bash -npx -y @modelcontextprotocol/inspector npx tsx index.js -``` +## Usage -Or make direct HTTP requests to test the REST API. +The bridge automatically spawns the real Context7 MCP server for each tool call and handles the stdio communication protocol. diff --git a/examples/mcp/mcp-servers/context7/index.js b/examples/mcp/mcp-servers/context7/index.js index f5f681c..543d046 100644 --- a/examples/mcp/mcp-servers/context7/index.js +++ b/examples/mcp/mcp-servers/context7/index.js @@ -1,594 +1,733 @@ /** - * MCP Context7 Server + * Context7 HTTP Bridge * - * This is a Model Context Protocol (MCP) server that provides library - * documentation and context resolution capabilities. It demonstrates how to - * build tools for fetching up-to-date documentation and resolving library IDs. - * It uses the official MCP TypeScript SDK and implements the proper MCP protocol - * with Streamable HTTP transport. + * This service provides an HTTP interface for the stdio-based Context7 MCP server from Upstash, + * allowing it to work with the Inference Gateway. It spawns the real Context7 MCP server + * as a child process and communicates via stdio using the MCP protocol. */ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import express from 'express'; +import cors from 'cors'; +import { spawn } from 'node:child_process'; import { randomUUID } from 'node:crypto'; -import { z } from 'zod'; // Express app for HTTP transport const app = express(); app.use(express.json()); +app.use(cors()); -// Map to store transports by session ID -const transports = {}; - -// Mock library registry for demonstration -const mockLibraryRegistry = [ - { - id: '/vercel/next.js', - name: 'Next.js', - description: 'The React Framework for Production', - trustScore: 9, - codeSnippets: 1250, - versions: ['14.2.0', '14.1.0', '14.0.0'], - documentation: 'https://nextjs.org/docs', - features: [ - 'Server-side rendering', - 'Static site generation', - 'API routes', - 'TypeScript support', - ], - examples: { - 'basic-app': 'npx create-next-app@latest my-app', - 'with-typescript': 'npx create-next-app@latest my-app --typescript', - 'api-route': 'Create API endpoints in pages/api/ directory', - }, - }, - { - id: '/facebook/react', - name: 'React', - description: 'A JavaScript library for building user interfaces', - trustScore: 10, - codeSnippets: 2100, - versions: ['18.3.0', '18.2.0', '18.1.0'], - documentation: 'https://react.dev', - features: ['Component-based', 'Virtual DOM', 'Hooks', 'JSX syntax'], - examples: { - 'functional-component': - 'function Component() { return
Hello
; }', - 'use-state': 'const [count, setCount] = useState(0);', - 'use-effect': 'useEffect(() => { /* side effect */ }, []);', - }, - }, - { - id: '/nodejs/node', - name: 'Node.js', - description: 'Node.js JavaScript runtime', - trustScore: 9, - codeSnippets: 850, - versions: ['20.12.0', '18.20.0', '16.20.0'], - documentation: 'https://nodejs.org/docs', - features: [ - 'Event-driven', - 'Non-blocking I/O', - 'NPM ecosystem', - 'Cross-platform', - ], - examples: { - 'http-server': - 'const http = require("http"); const server = http.createServer();', - 'file-system': - 'const fs = require("fs"); fs.readFile("file.txt", callback);', - 'express-app': - 'const express = require("express"); const app = express();', - }, - }, - { - id: '/microsoft/typescript', - name: 'TypeScript', - description: - 'TypeScript is a superset of JavaScript that compiles to plain JavaScript', - trustScore: 9, - codeSnippets: 1800, - versions: ['5.4.0', '5.3.0', '5.2.0'], - }, - { - id: '/expressjs/express', - name: 'Express', - description: 'Fast, unopinionated, minimalist web framework for Node.js', - trustScore: 8, - codeSnippets: 950, - versions: ['4.19.0', '4.18.0', '4.17.0'], - }, -]; - -// Mock documentation data -const mockDocumentation = { - '/vercel/next.js': { - title: 'Next.js App Router', - content: ` -# Next.js App Router - -The App Router is a new paradigm for building applications using React's latest features. - -## Basic Setup - -\`\`\`typescript -// app/page.tsx -export default function HomePage() { - return ( -
-

Welcome to Next.js

-

This is the app router in action!

-
- ) -} -\`\`\` - -## Server Components +// Map to store MCP sessions +const mcpSessions = new Map(); -Server Components run on the server and can fetch data directly: +// Context7 process instances cache +const context7Processes = new Map(); -\`\`\`typescript -// app/posts/page.tsx -async function getPosts() { - const res = await fetch('https://api.example.com/posts') - return res.json() -} +/** + * Create a Context7 process and manage its lifecycle + */ +class Context7Process { + constructor() { + this.process = null; + this.messageId = 0; + this.pendingRequests = new Map(); + this.isReady = false; + this.readyPromise = null; + } -export default async function PostsPage() { - const posts = await getPosts() - - return ( -
- {posts.map(post => ( -
-

{post.title}

-

{post.excerpt}

-
- ))} -
- ) -} -\`\`\` + async start() { + if (this.readyPromise) { + return this.readyPromise; + } -## Client Components + this.readyPromise = new Promise((resolve, reject) => { + console.info('🚀 Spawning Context7 MCP server...'); + + // Spawn the real Context7 MCP server + this.process = spawn('npx', ['-y', '@upstash/context7-mcp@latest'], { + stdio: ['pipe', 'pipe', 'pipe'], + env: { + ...process.env, + NODE_ENV: 'production', + }, + }); + + let buffer = ''; + + // Handle stdout - MCP protocol messages + this.process.stdout.on('data', (data) => { + buffer += data.toString(); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // Keep incomplete line in buffer + + for (const line of lines) { + if (line.trim()) { + try { + const message = JSON.parse(line.trim()); + this.handleMessage(message); + } catch { + console.warn('📝 Non-JSON output from Context7:', line.trim()); + } + } + } + }); + + // Handle stderr - logs and errors + this.process.stderr.on('data', (data) => { + const message = data.toString().trim(); + if (message.includes('ready') || message.includes('listening')) { + console.info('✅ Context7 MCP server ready'); + this.isReady = true; + resolve(); + } else { + console.info('📝 Context7 log:', message); + } + }); -Use "use client" directive for interactive components: + // Handle process exit + this.process.on('exit', (code) => { + console.info(`🔚 Context7 process exited with code ${code}`); + this.isReady = false; + this.process = null; -\`\`\`typescript -'use client' -import { useState } from 'react' + // Reject all pending requests + for (const [, { reject }] of this.pendingRequests) { + reject(new Error('Context7 process terminated')); + } + this.pendingRequests.clear(); + }); + + // Handle errors + this.process.on('error', (error) => { + console.error('❌ Context7 process error:', error); + reject(error); + }); // Initialize the MCP session + globalThis.setTimeout(() => { + this.sendInitialize(); + }, 2000); + }); + + return this.readyPromise; + } -export default function Counter() { - const [count, setCount] = useState(0) - - return ( - - ) -} -\`\`\` - `, - }, - '/facebook/react': { - title: 'React Hooks and Components', - content: ` -# React Hooks Guide - -## useState Hook - -\`\`\`typescript -import { useState } from 'react' - -function Counter() { - const [count, setCount] = useState(0) - - return ( -
-

You clicked {count} times

- -
- ) -} -\`\`\` - -## useEffect Hook - -\`\`\`typescript -import { useState, useEffect } from 'react' - -function DataFetcher() { - const [data, setData] = useState(null) - const [loading, setLoading] = useState(true) - - useEffect(() => { - fetch('/api/data') - .then(res => res.json()) - .then(data => { - setData(data) - setLoading(false) + sendInitialize() { + console.info('🔧 Sending initialize to Context7...'); + const initMessage = { + jsonrpc: '2.0', + id: this.nextMessageId(), + method: 'initialize', + params: { + protocolVersion: '0.1.0', + capabilities: { + tools: {}, + }, + clientInfo: { + name: 'context7-bridge', + version: '1.0.0', + }, + }, + }; + + // Track this request properly + const messageId = initMessage.id; + const initPromise = new Promise((resolve, reject) => { + this.pendingRequests.set(messageId, { resolve, reject }); + + // Set timeout for initialization + globalThis.setTimeout(() => { + if (this.pendingRequests.has(messageId)) { + this.pendingRequests.delete(messageId); + reject(new Error('Context7 initialization timeout')); + } + }, 10000); + }); + + this.sendMessage(initMessage); + + // Handle the initialization response + initPromise + .then(() => { + console.info('✅ Context7 initialized successfully'); + this.isReady = true; + // Send initialized notification after successful init + globalThis.setTimeout(() => { + this.sendMessage({ + jsonrpc: '2.0', + method: 'notifications/initialized', + }); + }, 100); }) - }, []) - - if (loading) return
Loading...
- - return
{JSON.stringify(data)}
-} -\`\`\` + .catch((error) => { + console.error('❌ Context7 initialization failed:', error); + }); + } -## Custom Hooks + nextMessageId() { + return ++this.messageId; + } -\`\`\`typescript -function useLocalStorage(key: string, initialValue: any) { - const [storedValue, setStoredValue] = useState(() => { - try { - const item = window.localStorage.getItem(key) - return item ? JSON.parse(item) : initialValue - } catch (error) { - return initialValue - } - }) - - const setValue = (value: any) => { - try { - setStoredValue(value) - window.localStorage.setItem(key, JSON.stringify(value)) - } catch (error) { - console.log(error) + sendMessage(message) { + if (!this.process || !this.process.stdin.writable) { + throw new Error('Context7 process not available'); } + + const jsonMessage = JSON.stringify(message) + '\n'; + console.info('📤 Sending to Context7:', JSON.stringify(message, null, 2)); + this.process.stdin.write(jsonMessage); } - - return [storedValue, setValue] -} -\`\`\` - `, - }, -}; -/** - * Create and configure the MCP server - */ -function createMcpServer() { - const mcpServer = new McpServer({ - name: 'context7', - version: '1.0.0', - }); + handleMessage(message) { + console.info( + '📥 Received from Context7:', + JSON.stringify(message, null, 2) + ); + + // Handle responses to our requests + if (message.id && this.pendingRequests.has(message.id)) { + const { resolve, reject } = this.pendingRequests.get(message.id); + this.pendingRequests.delete(message.id); + + if (message.error) { + console.error('❌ Context7 error response:', message.error); + reject(new Error(message.error.message || 'Context7 error')); + return; + } + + console.info('✅ Context7 success response for ID', message.id); + resolve(message.result || message); + return; + } - // Tool: Search libraries - mcpServer.tool( - 'search_libraries', - { - query: z - .string() - .describe('Search query for libraries (name, description, or ID)'), - limit: z - .number() - .optional() - .describe('Maximum number of results to return (default: 10)'), - }, - async ({ query, limit = 10 }) => { + // Special handling for initialization - Context7 doesn't send notifications/initialized + if ( + message.result && + message.result.serverInfo && + message.result.serverInfo.name === 'Context7' + ) { console.info( - `Searching libraries with query: "${query}", limit: ${limit}` + '✅ Context7 initialized successfully (detected from serverInfo)' ); + this.isReady = true; + return; + } - try { - const searchTerm = query.toLowerCase(); - const results = mockLibraryRegistry - .filter( - (lib) => - lib.name.toLowerCase().includes(searchTerm) || - lib.description.toLowerCase().includes(searchTerm) || - lib.id.toLowerCase().includes(searchTerm) - ) - .slice(0, limit) - .map((lib) => ({ - id: lib.id, - name: lib.name, - description: lib.description, - trustScore: lib.trustScore, - latestVersion: lib.versions[0], - codeSnippets: lib.codeSnippets, - })); - - if (results.length === 0) { - return { - content: [ - { - type: 'text', - text: `No libraries found matching query: "${query}"`, - }, - ], - }; + // Handle specific notifications and responses + switch (message.method) { + case 'notifications/initialized': + console.info('✅ Context7 initialized notification received'); + this.isReady = true; + break; + default: + if (message.method) { + console.info('📢 Context7 notification/method:', message.method); + } else if (message.id) { + console.warn( + '⚠️ Received response for unknown request ID:', + message.id + ); + } else { + console.info('ℹ️ Context7 message (no ID or method)'); } + } + } - const resultText = results - .map( - (lib) => - `📚 ${lib.name} (${lib.id})\n` + - `Description: ${lib.description}\n` + - `Latest Version: ${lib.latestVersion}\n` + - `Trust Score: ${lib.trustScore}/10\n` + - `Code Snippets: ${lib.codeSnippets}\n` - ) - .join('\n---\n'); - - return { - content: [ - { - type: 'text', - text: `Found ${results.length} libraries:\n\n${resultText}`, - }, - ], - }; - } catch (error) { - console.error('Error searching libraries:', error.message); - return { - content: [ - { - type: 'text', - text: `Error searching libraries: ${error.message}`, - }, - ], - }; - } + async callTool(name, args) { + if (!this.isReady) { + await this.start(); } - ); - - // Tool: Get library details - mcpServer.tool( - 'get_library_details', - { - libraryId: z.string().describe('The library ID to get details for'), - }, - async ({ libraryId }) => { - console.info(`Getting details for library: ${libraryId}`); - - try { - const library = mockLibraryRegistry.find((lib) => lib.id === libraryId); - - if (!library) { - return { - content: [ - { - type: 'text', - text: `Library not found: ${libraryId}`, - }, - ], - }; + + const messageId = this.nextMessageId(); + const message = { + jsonrpc: '2.0', + id: messageId, + method: 'tools/call', + params: { + name, + arguments: args, + }, + }; + + return new Promise((resolve, reject) => { + this.pendingRequests.set(messageId, { resolve, reject }); + + // Set timeout for the request + globalThis.setTimeout(() => { + if (this.pendingRequests.has(messageId)) { + this.pendingRequests.delete(messageId); + reject(new Error('Context7 request timeout')); } + }, 30000); // 30 second timeout - const detailsText = - `📚 ${library.name}\n\n` + - `ID: ${library.id}\n` + - `Description: ${library.description}\n` + - `Trust Score: ${library.trustScore}/10\n` + - `Code Snippets Available: ${library.codeSnippets}\n` + - `Documentation: ${library.documentation}\n\n` + - `Available Versions:\n${library.versions.map((v) => ` • ${v}`).join('\n')}\n\n` + - `Key Features:\n${library.features.map((f) => ` • ${f}`).join('\n')}\n\n` + - `Code Examples:\n${Object.entries(library.examples) - .map(([key, example]) => ` ${key}: ${example}`) - .join('\n')}`; - - return { - content: [ - { - type: 'text', - text: detailsText, - }, - ], - }; - } catch (error) { - console.error('Error getting library details:', error.message); - return { - content: [ - { - type: 'text', - text: `Error getting library details: ${error.message}`, - }, - ], - }; - } + this.sendMessage(message); + }); + } + + requestToolsList() { + console.info('🔍 Requesting tools list from Context7...'); + const toolsListMessage = { + jsonrpc: '2.0', + id: this.nextMessageId(), + method: 'tools/list', + params: {}, + }; + + // Track this request properly + const messageId = toolsListMessage.id; + const toolsPromise = new Promise((resolve, reject) => { + this.pendingRequests.set(messageId, { resolve, reject }); + + // Set timeout for tools list request + globalThis.setTimeout(() => { + if (this.pendingRequests.has(messageId)) { + this.pendingRequests.delete(messageId); + reject(new Error('Context7 tools list timeout')); + } + }, 5000); + }); + + this.sendMessage(toolsListMessage); + + // Handle the tools list response + toolsPromise + .then((result) => { + console.info( + '✅ Context7 tools list received:', + JSON.stringify(result, null, 2) + ); + }) + .catch((error) => { + console.error('❌ Context7 tools list failed:', error); + }); + } + + async listTools() { + if (!this.isReady) { + await this.start(); } - ); - - // Tool: Get documentation - mcpServer.tool( - 'get_documentation', - { - libraryId: z.string().describe('The library ID to get documentation for'), - section: z - .string() - .optional() - .describe( - 'Specific documentation section (gettingStarted, quickStart, bestPractices, commonIssues)' - ), - }, - async ({ libraryId, section }) => { - console.info( - `Getting documentation for library: ${libraryId}, section: ${section || 'all'}` - ); - try { - const docs = mockDocumentation[libraryId]; + const messageId = this.nextMessageId(); + const message = { + jsonrpc: '2.0', + id: messageId, + method: 'tools/list', + }; - if (!docs) { - return { - content: [ - { - type: 'text', - text: `Documentation not available for library: ${libraryId}`, - }, - ], - }; + return new Promise((resolve, reject) => { + this.pendingRequests.set(messageId, { resolve, reject }); + + globalThis.setTimeout(() => { + if (this.pendingRequests.has(messageId)) { + this.pendingRequests.delete(messageId); + reject(new Error('Context7 list tools timeout')); } + }, 10000); + + this.sendMessage(message); + }); + } - let docText = `📖 Documentation for ${libraryId}\n\n${docs.title}\n\n${docs.content}`; + terminate() { + if (this.process) { + console.info('🔄 Terminating Context7 process...'); + this.process.kill('SIGTERM'); - return { - content: [ + // Force kill after 5 seconds + globalThis.setTimeout(() => { + if (this.process) { + console.info('🔪 Force killing Context7 process...'); + this.process.kill('SIGKILL'); + } + }, 5000); + } + } +} + +/** + * Get or create a Context7 process instance + */ +function getContext7Process(sessionId = 'default') { + if (!context7Processes.has(sessionId)) { + const process = new Context7Process(); + context7Processes.set(sessionId, process); + + // Clean up after 10 minutes of inactivity + globalThis.setTimeout( + () => { + if (context7Processes.has(sessionId)) { + const proc = context7Processes.get(sessionId); + proc.terminate(); + context7Processes.delete(sessionId); + } + }, + 10 * 60 * 1000 + ); + } + + return context7Processes.get(sessionId); +} + +/** + * Handle MCP request directly without SDK Server + */ +async function handleMcpRequest(request) { + if (!request || !request.method) { + throw new Error('Invalid request: missing method'); + } + + switch (request.method) { + case 'initialize': { + console.info( + '🔧 MCP initialize request:', + JSON.stringify(request, null, 2) + ); + + return { + jsonrpc: '2.0', + id: request.id, + result: { + protocolVersion: '0.1.0', + capabilities: { + tools: {}, + }, + serverInfo: { + name: 'context7-bridge', + version: '1.0.0', + }, + }, + }; + } + + case 'tools/list': { + console.info('📋 MCP tools/list request'); + + return { + jsonrpc: '2.0', + id: request.id, + result: { + tools: [ { - type: 'text', - text: docText, + name: 'c41_resolve-library-id', + description: + 'Resolve library names to Context7-compatible library IDs', + inputSchema: { + type: 'object', + properties: { + libraryName: { + type: 'string', + description: + 'Library name to search for and retrieve a Context7-compatible library ID', + }, + }, + required: ['libraryName'], + }, }, - ], - }; - } catch (error) { - console.error('Error getting documentation:', error.message); - return { - content: [ { - type: 'text', - text: `Error getting documentation: ${error.message}`, + name: 'c41_get-library-docs', + description: 'Fetch up-to-date library documentation', + inputSchema: { + type: 'object', + properties: { + context7CompatibleLibraryID: { + type: 'string', + description: + 'Exact Context7-compatible library ID (e.g., "/mongodb/docs", "/vercel/next.js")', + }, + tokens: { + type: 'number', + description: + 'Maximum number of tokens of documentation to retrieve', + default: 10000, + }, + topic: { + type: 'string', + description: + 'Topic to focus documentation on (e.g., "hooks", "routing")', + }, + }, + required: ['context7CompatibleLibraryID'], + }, }, ], - }; - } + }, + }; } - ); - - // Tool: Resolve library ID - mcpServer.tool( - 'resolve_library_id', - { - libraryName: z.string().describe('The library name to resolve to an ID'), - }, - async ({ libraryName }) => { - console.info(`Resolving library ID for: ${libraryName}`); - - try { - const searchTerm = libraryName.toLowerCase(); - const matches = mockLibraryRegistry.filter( - (lib) => - lib.name.toLowerCase() === searchTerm || - lib.name.toLowerCase().includes(searchTerm) || - lib.id.toLowerCase().includes(searchTerm) - ); - if (matches.length === 0) { - return { - content: [ - { - type: 'text', - text: `No library found with name: "${libraryName}"`, + case 'tools/call': { + console.info( + '🔧 MCP tools/call request:', + JSON.stringify(request, null, 2) + ); + + if (!request?.params?.name) { + throw new Error('Missing tool name in request'); + } + + const { name, arguments: args } = request.params; + + switch (name) { + case 'c41_resolve-library-id': { + console.info(`🔍 Resolving library ID for: ${args.libraryName}`); + console.info(`📝 Input arguments:`, JSON.stringify(args, null, 2)); + + try { + const context7 = getContext7Process(); + console.info(`🚀 Calling Context7 resolve-library-id tool...`); + + const result = await context7.callTool('resolve-library-id', { + libraryName: args.libraryName, + }); + + console.info( + `✅ Context7 resolve-library-id raw result:`, + JSON.stringify(result, null, 2) + ); + console.info(`📊 Result structure analysis:`); + console.info(` - Result type: ${typeof result}`); + console.info( + ` - Has content array: ${Array.isArray(result.content)}` + ); + console.info(` - Content length: ${result.content?.length || 0}`); + + if (result.content?.[0]) { + console.info( + ` - First content item type: ${result.content[0].type}` + ); + console.info( + ` - First content text length: ${result.content[0].text?.length || 0}` + ); + console.info( + ` - First content text preview: ${result.content[0].text?.substring(0, 200)}...` + ); + } + + const responseText = + result.content?.[0]?.text || JSON.stringify(result, null, 2); + console.info( + `📤 Sending response text (${responseText.length} chars):`, + responseText.substring(0, 500) + + (responseText.length > 500 ? '...' : '') + ); + + return { + jsonrpc: '2.0', + id: request.id, + result: { + content: [ + { + type: 'text', + text: responseText, + }, + ], }, - ], - }; + }; + } catch (error) { + console.error('❌ Error resolving library ID:', error); + console.error('❌ Error stack:', error.stack); + console.error('❌ Error details:', { + name: error.name, + message: error.message, + cause: error.cause, + }); + + const errorText = `Error resolving library ID for "${args.libraryName}": ${error.message}`; + console.info(`📤 Sending error response:`, errorText); + + return { + jsonrpc: '2.0', + id: request.id, + result: { + content: [ + { + type: 'text', + text: errorText, + }, + ], + }, + }; + } } - const exactMatch = matches.find( - (lib) => lib.name.toLowerCase() === searchTerm - ); - const bestMatch = exactMatch || matches[0]; - - let resultText = - `🔍 Resolved "${libraryName}" to:\n\n` + - `ID: ${bestMatch.id}\n` + - `Name: ${bestMatch.name}\n` + - `Description: ${bestMatch.description}\n`; - - if (matches.length > 1) { - const otherMatches = matches - .filter((lib) => lib.id !== bestMatch.id) - .map((lib) => ` • ${lib.name} (${lib.id})`) - .join('\n'); - resultText += `\nOther possible matches:\n${otherMatches}`; + case 'c41_get-library-docs': { + console.info( + `📚 Getting documentation for: ${args.context7CompatibleLibraryID}` + ); + console.info(`📝 Input arguments:`, JSON.stringify(args, null, 2)); + + try { + const context7 = getContext7Process(); + console.info(`🚀 Calling Context7 get-library-docs tool...`); + + const callArgs = { + context7CompatibleLibraryID: args.context7CompatibleLibraryID, + tokens: args.tokens || 10000, + }; + + if (args.topic) { + callArgs.topic = args.topic; + } + + console.info( + `📝 Context7 call arguments:`, + JSON.stringify(callArgs, null, 2) + ); + + const result = await context7.callTool( + 'get-library-docs', + callArgs + ); + + console.info( + `✅ Context7 get-library-docs raw result:`, + JSON.stringify(result, null, 2) + ); + console.info(`📊 Result structure analysis:`); + console.info(` - Result type: ${typeof result}`); + console.info( + ` - Has content array: ${Array.isArray(result.content)}` + ); + console.info(` - Content length: ${result.content?.length || 0}`); + + if (result.content?.[0]) { + console.info( + ` - First content item type: ${result.content[0].type}` + ); + console.info( + ` - First content text length: ${result.content[0].text?.length || 0}` + ); + console.info( + ` - First content text preview: ${result.content[0].text?.substring(0, 200)}...` + ); + } + + const responseText = + result.content?.[0]?.text || JSON.stringify(result, null, 2); + console.info( + `📤 Sending response text (${responseText.length} chars):`, + responseText.substring(0, 500) + + (responseText.length > 500 ? '...' : '') + ); + + return { + jsonrpc: '2.0', + id: request.id, + result: { + content: [ + { + type: 'text', + text: responseText, + }, + ], + }, + }; + } catch (error) { + console.error('❌ Error getting library documentation:', error); + console.error('❌ Error stack:', error.stack); + console.error('❌ Error details:', { + name: error.name, + message: error.message, + cause: error.cause, + }); + + const errorText = `Error getting documentation for "${args.context7CompatibleLibraryID}": ${error.message}`; + console.info(`📤 Sending error response:`, errorText); + + return { + jsonrpc: '2.0', + id: request.id, + result: { + content: [ + { + type: 'text', + text: errorText, + }, + ], + }, + }; + } } - return { - content: [ - { - type: 'text', - text: resultText, - }, - ], - }; - } catch (error) { - console.error('Error resolving library ID:', error.message); - return { - content: [ - { - type: 'text', - text: `Error resolving library ID: ${error.message}`, - }, - ], - }; + default: + throw new Error(`Unknown tool: ${name}`); } } - ); - return mcpServer; + default: + throw new Error(`Unknown method: ${request.method}`); + } } /** - * Set up session routes for MCP protocol + * Setup MCP endpoints for proper Model Context Protocol communication */ function setupSessionRoutes() { // Handle POST requests for MCP communication app.post('/mcp', async (req, res) => { try { - console.info('MCP POST request received:'); + console.info('📨 MCP POST request received:'); console.info(' Headers: %s', JSON.stringify(req.headers, null, 2)); console.info(' Body: %s', JSON.stringify(req.body, null, 2)); - // Fix missing Accept headers for compatibility with Go MCP clients - // The StreamableHTTPServerTransport requires both application/json and text/event-stream - const accept = req.headers.accept || req.headers.Accept; - if ( - !accept || - !accept.includes('application/json') || - !accept.includes('text/event-stream') - ) { - console.info('Adding missing Accept headers for MCP compatibility'); - req.headers.accept = 'application/json, text/event-stream'; + // Validate request body + if (!req.body || typeof req.body !== 'object') { + return res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32600, + message: 'Invalid Request - missing or invalid request body', + }, + id: null, + }); } - // Check for existing session ID - const sessionId = req.headers['mcp-session-id']; - let transport; - - if (sessionId && transports[sessionId]) { - // Reuse existing transport - transport = transports[sessionId]; - } else { - // Create new transport for new session - transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - onsessioninitialized: (newSessionId) => { - console.info(`MCP session initialized: ${newSessionId}`); - // Store the transport by session ID - transports[newSessionId] = transport; + if (!req.body.jsonrpc || !req.body.method) { + return res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32600, + message: 'Invalid Request - missing jsonrpc or method field', }, + id: req.body.id || null, }); + } - // Clean up transport when closed - transport.onclose = () => { - if (transport.sessionId) { - console.info(`MCP session closed: ${transport.sessionId}`); - delete transports[transport.sessionId]; - } - }; + // Get or create session + const sessionId = req.headers['mcp-session-id'] || randomUUID(); - // Create and connect MCP server - const server = createMcpServer(); - await server.connect(transport); + if (!mcpSessions.has(sessionId)) { + mcpSessions.set(sessionId, { + createdAt: Date.now(), + }); + console.info(`🎯 MCP session created: ${sessionId}`); + + // Set session cleanup timer + globalThis.setTimeout( + () => { + if (mcpSessions.has(sessionId)) { + mcpSessions.delete(sessionId); + console.info(`🧹 Cleaned up session: ${sessionId}`); + } + }, + 10 * 60 * 1000 + ); // 10 minutes } // Handle the MCP request - await transport.handleRequest(req, res, req.body); + const response = await handleMcpRequest(req.body); + + // Set session ID header in response + res.setHeader('mcp-session-id', sessionId); + res.json(response); } catch (error) { - console.error('Error handling MCP request:', error); + console.error('❌ Error handling MCP request:', error); + if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', + data: error.message, }, - id: null, + id: req.body?.id || null, }); } } @@ -597,39 +736,75 @@ function setupSessionRoutes() { // Handle GET requests for SSE (server-to-client notifications) app.get('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id']; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); - return; + + if (!sessionId || !mcpSessions.has(sessionId)) { + return res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32600, + message: 'Invalid or missing session ID', + }, + }); } - const transport = transports[sessionId]; - await transport.handleRequest(req, res); + // Set up SSE stream + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'mcp-session-id', + }); + + res.write('data: {"type":"connected"}\n\n'); + + // Keep connection alive + const keepAlive = globalThis.setInterval(() => { + res.write('data: {"type":"ping"}\n\n'); + }, 30000); + + req.on('close', () => { + globalThis.clearInterval(keepAlive); + }); }); // Handle DELETE requests for session termination app.delete('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id']; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); - return; + + if (!sessionId || !mcpSessions.has(sessionId)) { + return res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32600, + message: 'Invalid or missing session ID', + }, + }); } - const transport = transports[sessionId]; - await transport.handleRequest(req, res); + // Clean up session + mcpSessions.delete(sessionId); + + console.info(`🗑️ Session terminated: ${sessionId}`); + res.status(200).json({ + jsonrpc: '2.0', + result: { status: 'terminated' }, + }); }); } // Health check endpoint -app.get('/health', (req, res) => { +app.get('/health', (_req, res) => { const healthStatus = { status: 'healthy', - server: 'mcp-context7', + server: 'context7-bridge', version: '1.0.0', - activeSessions: Object.keys(transports).length, - availableLibraries: mockLibraryRegistry.length, + activeSessions: mcpSessions.size, + activeProcesses: context7Processes.size, + timestamp: new Date().toISOString(), }; - console.info('Health check requested: %j', healthStatus); + console.info('💚 Health check requested: %j', healthStatus); res.json(healthStatus); }); @@ -637,17 +812,17 @@ app.get('/health', (req, res) => { * Start the server */ async function startServer() { - const port = process.env.PORT || 3002; + const PORT = process.env.PORT || 3002; const host = process.env.HOST || '0.0.0.0'; // Set up session routes setupSessionRoutes(); - app.listen(port, host, async () => { - console.info(`MCP Context7 server running on http://${host}:${port}`); - console.info('Protocol: Model Context Protocol (MCP)'); - console.info('Transport: Streamable HTTP'); - console.info('Available endpoints:'); + app.listen(PORT, host, () => { + console.info(`🌉 Context7 HTTP Bridge running on http://${host}:${PORT}`); + console.info('📋 Protocol: Model Context Protocol (MCP) HTTP Bridge'); + console.info('🎯 Target: Context7 MCP Server (stdio)'); + console.info('🔗 Available endpoints:'); console.info(' POST /mcp - MCP protocol endpoint'); console.info( ' GET /mcp - SSE notifications (with session-id header)' @@ -656,41 +831,48 @@ async function startServer() { ' DELETE /mcp - Session termination (with session-id header)' ); console.info(' GET /health - Health check'); - console.info('Available tools:'); + console.info('🛠️ Available tools:'); console.info( - ' - search_libraries - Search for libraries by name or description' + ' - c41_resolve-library-id - Resolve library names to Context7 IDs' ); console.info( - ' - get_library_details - Get detailed information about a specific library' + ' - c41_get-library-docs - Fetch up-to-date library documentation' ); - console.info(' - get_documentation - Get documentation for a library'); - console.info(' - resolve_library_id - Resolve a library name to its ID'); - console.info(`Available libraries: ${mockLibraryRegistry.length}`); - console.info('MCP Context7 server ready for connections'); + console.info('🚀 Bridge ready for connections'); }); } // Graceful shutdown process.on('SIGTERM', () => { - console.info('Received SIGTERM, shutting down gracefully'); - // Close all transports - Object.values(transports).forEach((transport) => { - if (transport.close) transport.close(); + console.info('🔄 Received SIGTERM, shutting down gracefully'); + + // Clear all MCP sessions + mcpSessions.clear(); + + // Terminate all Context7 processes + context7Processes.forEach((proc) => { + proc.terminate(); }); + process.exit(0); }); process.on('SIGINT', () => { - console.info('Received SIGINT, shutting down gracefully'); - // Close all transports - Object.values(transports).forEach((transport) => { - if (transport.close) transport.close(); + console.info('🔄 Received SIGINT, shutting down gracefully'); + + // Clear all MCP sessions + mcpSessions.clear(); + + // Terminate all Context7 processes + context7Processes.forEach((proc) => { + proc.terminate(); }); + process.exit(0); }); // Start the server startServer().catch((error) => { - console.error('Failed to start server:', error); + console.error('💥 Failed to start server:', error); process.exit(1); }); diff --git a/examples/mcp/mcp-servers/context7/package-lock.json b/examples/mcp/mcp-servers/context7/package-lock.json index 9c1c899..4f52616 100644 --- a/examples/mcp/mcp-servers/context7/package-lock.json +++ b/examples/mcp/mcp-servers/context7/package-lock.json @@ -1,25 +1,50 @@ { - "name": "mcp-context7-server", + "name": "context7-http-bridge", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "mcp-context7-server", + "name": "context7-http-bridge", "version": "1.0.0", - "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.0", - "axios": "^1.6.0", + "@modelcontextprotocol/sdk": "^0.5.0", + "@upstash/context7-mcp": "latest", "cors": "^2.8.5", + "dotenv": "^16.3.1", "express": "^4.18.2", - "zod": "^3.22.0" + "zod": "^3.22.4" }, - "engines": { - "node": ">=18.0.0" + "devDependencies": { + "nodemon": "^3.0.1" } }, "node_modules/@modelcontextprotocol/sdk": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.5.0.tgz", + "integrity": "sha512-RXgulUX6ewvxjAG0kOpLMEdXXWkzWgaoCGaA2CwNW7cQCIphjpJhjpHSiaPdVCnisjRF/0Cm9KWHUuIoeiAblQ==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@upstash/context7-mcp": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@upstash/context7-mcp/-/context7-mcp-1.0.12.tgz", + "integrity": "sha512-l1l0iSnpHJMgEnsLcf7OEqD3MmirXUb13R4f6hLUD026KaUT54aYXPFS7XgYZ/Rfl2q3ArjX/7N5l87amzyP+g==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.8.0", + "dotenv": "^16.5.0", + "zod": "^3.24.2" + }, + "bin": { + "context7-mcp": "dist/index.js" + } + }, + "node_modules/@upstash/context7-mcp/node_modules/@modelcontextprotocol/sdk": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz", "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==", @@ -41,7 +66,7 @@ "node": ">=18" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "node_modules/@upstash/context7-mcp/node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", @@ -54,7 +79,7 @@ "node": ">= 0.6" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "node_modules/@upstash/context7-mcp/node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", @@ -74,7 +99,7 @@ "node": ">=18" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "node_modules/@upstash/context7-mcp/node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", @@ -86,7 +111,7 @@ "node": ">= 0.6" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "node_modules/@upstash/context7-mcp/node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", @@ -95,7 +120,7 @@ "node": ">=6.6.0" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "node_modules/@upstash/context7-mcp/node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", @@ -112,7 +137,7 @@ } } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "node_modules/@upstash/context7-mcp/node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", @@ -154,7 +179,7 @@ "url": "https://opencollective.com/express" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "node_modules/@upstash/context7-mcp/node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", @@ -171,7 +196,7 @@ "node": ">= 0.8" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "node_modules/@upstash/context7-mcp/node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", @@ -180,7 +205,7 @@ "node": ">= 0.8" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "node_modules/@upstash/context7-mcp/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", @@ -192,7 +217,7 @@ "node": ">=0.10.0" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "node_modules/@upstash/context7-mcp/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", @@ -201,7 +226,7 @@ "node": ">= 0.8" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "node_modules/@upstash/context7-mcp/node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", @@ -213,7 +238,7 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "node_modules/@upstash/context7-mcp/node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", @@ -222,7 +247,7 @@ "node": ">= 0.6" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "node_modules/@upstash/context7-mcp/node_modules/mime-types": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", @@ -234,13 +259,13 @@ "node": ">= 0.6" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "node_modules/@upstash/context7-mcp/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "node_modules/@upstash/context7-mcp/node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", @@ -249,7 +274,7 @@ "node": ">= 0.6" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "node_modules/@upstash/context7-mcp/node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", @@ -264,7 +289,7 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "node_modules/@upstash/context7-mcp/node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", @@ -286,7 +311,7 @@ "node": ">= 18" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "node_modules/@upstash/context7-mcp/node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", @@ -301,7 +326,7 @@ "node": ">= 18" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "node_modules/@upstash/context7-mcp/node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", @@ -344,27 +369,44 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, - "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/body-parser": { @@ -406,6 +448,30 @@ "node": ">= 0.8" } }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -444,18 +510,38 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -528,15 +614,6 @@ "ms": "2.0.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -556,6 +633,18 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -615,21 +704,6 @@ "node": ">= 0.4" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -739,6 +813,19 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -757,41 +844,6 @@ "node": ">= 0.8" } }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -810,6 +862,21 @@ "node": ">= 0.6" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -856,6 +923,19 @@ "node": ">= 0.4" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -868,6 +948,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -880,21 +970,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -935,6 +1010,13 @@ "node": ">=0.10.0" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -950,6 +1032,52 @@ "node": ">= 0.10" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -1037,6 +1165,19 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1052,6 +1193,70 @@ "node": ">= 0.6" } }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1118,6 +1323,19 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkce-challenge": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", @@ -1140,10 +1358,11 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, "license": "MIT" }, "node_modules/punycode": { @@ -1206,6 +1425,19 @@ "node": ">=0.10.0" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1280,6 +1512,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -1433,6 +1678,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1442,6 +1700,32 @@ "node": ">= 0.8" } }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1451,6 +1735,16 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1464,6 +1758,13 @@ "node": ">= 0.6" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1522,9 +1823,9 @@ "license": "ISC" }, "node_modules/zod": { - "version": "3.25.42", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.42.tgz", - "integrity": "sha512-PcALTLskaucbeHc41tU/xfjfhcz8z0GdhhDcSgrCTmSazUuqnYqiXO63M0QUBVwpBlsLsNVn5qHSC5Dw3KZvaQ==", + "version": "3.25.43", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.43.tgz", + "integrity": "sha512-KrQFEkfox9WLfYCOksqEWjf97QdgSC1YJSqxJxnWz0l+BW8mN2PQ+yjshL8RC7RC5p5YOlUCLyTDNaf++QbPtA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/examples/mcp/mcp-servers/context7/package.json b/examples/mcp/mcp-servers/context7/package.json index 7506c0e..6c487dc 100644 --- a/examples/mcp/mcp-servers/context7/package.json +++ b/examples/mcp/mcp-servers/context7/package.json @@ -1,23 +1,21 @@ { - "name": "mcp-context7-server", + "name": "context7-http-bridge", "version": "1.0.0", - "description": "MCP Context7 Server for library documentation and context resolution", - "type": "module", + "description": "HTTP bridge for stdio-based Context7 MCP server using official MCP SDK", "main": "index.js", + "type": "module", "scripts": { "start": "node index.js", - "dev": "node --watch index.js" + "dev": "nodemon index.js" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.0", - "axios": "^1.6.0", - "cors": "^2.8.5", "express": "^4.18.2", - "zod": "^3.22.0" - }, - "engines": { - "node": ">=18.0.0" + "cors": "^2.8.5", + "@upstash/context7-mcp": "latest", + "@modelcontextprotocol/sdk": "^0.5.0", + "zod": "^3.22.4" }, - "author": "Inference Gateway Team", - "license": "MIT" + "devDependencies": { + "nodemon": "^3.0.1" + } } diff --git a/examples/mcp/mcp-servers/npm/index.js b/examples/mcp/mcp-servers/npm/index.js index c5c31c5..20d0a37 100644 --- a/examples/mcp/mcp-servers/npm/index.js +++ b/examples/mcp/mcp-servers/npm/index.js @@ -93,7 +93,7 @@ async function executeNpmCommand(command, cwd = workingDirectory) { try { const { stdout, stderr } = await execAsync(fullCommand, { cwd, - timeout: 30000, // 30 second timeout + timeout: 300000, // 5 minute timeout (increased from 30 seconds) maxBuffer: 1024 * 1024, // 1MB buffer }); @@ -367,7 +367,7 @@ function createMcpServer() { try { // Build the npx create-next-app command with options - let command = `npx create-next-app@latest "${name}"`; + let command = `npx create-next-app@latest "${name}" --yes`; // Add flags based on options if (typescript) { @@ -408,7 +408,7 @@ function createMcpServer() { const { stdout, stderr } = await execAsync(command, { cwd: workDir, - timeout: 180000, // 3 minute timeout for project creation + timeout: 600000, // 10 minute timeout for project creation (increased from 3 minutes) maxBuffer: 1024 * 1024 * 5, // 5MB buffer }); diff --git a/examples/mcp/package.json b/examples/mcp/package.json index 29adf05..6251637 100644 --- a/examples/mcp/package.json +++ b/examples/mcp/package.json @@ -5,8 +5,8 @@ "main": "index.js", "private": true, "scripts": { + "clean": "rm -rf shared/next-app shared/update-check", "start": "tsx example-basic.ts", - "example": "tsx example-${npm_config_name:-basic}.ts", "example:basic": "tsx example-basic.ts", "example:advanced": "tsx example-advanced.ts", "example:nextjs": "tsx example-nextjs.ts", @@ -16,7 +16,7 @@ "example:test-direct-mcp": "tsx example-test-direct-mcp.ts", "example:list-tools": "tsx example-list-tools.ts", "example:mcp-tools": "tsx example-mcp-tools.ts", - "example:context7": "tsx example-context7.ts", + "example:agent": "tsx example-agent.ts", "compose:up": "docker-compose up -d", "compose:down": "docker-compose down", "compose:logs": "docker-compose logs -f" diff --git a/src/client.ts b/src/client.ts index 26f113d..0ad12aa 100644 --- a/src/client.ts +++ b/src/client.ts @@ -280,7 +280,7 @@ export class InferenceGatewayClient { this.apiKey = options.apiKey; this.defaultHeaders = options.defaultHeaders || {}; this.defaultQuery = options.defaultQuery || {}; - this.timeout = options.timeout || 30000; + this.timeout = options.timeout || 60000; // Increased default timeout to 60 seconds this.fetchFn = options.fetch || globalThis.fetch; } From aff44312a145ab864624994dac5dc96c8fd3e7f9 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 11:15:50 +0000 Subject: [PATCH 13/38] chore(mcp-agent-example): Enhance retry logic and improve system prompt for Context7 agent I need to fix in the Inference Gateway or find a solution for DeepSeek 30seconds timeout - it's annoying. Signed-off-by: Eden Reich --- examples/mcp/example-agent.ts | 447 +++++++++++++++++++--------------- 1 file changed, 253 insertions(+), 194 deletions(-) diff --git a/examples/mcp/example-agent.ts b/examples/mcp/example-agent.ts index 485349e..e7a4b63 100644 --- a/examples/mcp/example-agent.ts +++ b/examples/mcp/example-agent.ts @@ -17,7 +17,6 @@ import { // Load environment variables from the mcp directory dotenv.config({ path: path.join(__dirname, '.env') }); -// For ES modules compatibility declare const require: any; declare const module: any; @@ -26,6 +25,8 @@ interface AgentConfig { provider: Provider; model: string; conversationHistory: Array<{ role: MessageRole; content: string }>; + maxRetries: number; + retryDelayMs: number; } class Context7Agent { @@ -40,6 +41,8 @@ class Context7Agent { provider: (process.env.PROVIDER as Provider) || Provider.groq, model: process.env.LLM || 'llama-3.3-70b-versatile', conversationHistory: [], + maxRetries: 3, + retryDelayMs: 60000, // 1 minute }; this.rl = readline.createInterface({ @@ -54,93 +57,107 @@ class Context7Agent { }); } + private async delay(ms: number): Promise { + return new Promise((resolve) => { + global.setTimeout(() => resolve(), ms); + }); + } + private getSystemPrompt(): string { - return `You are an expert software development assistant with access to Context7 MCP tools for library documentation and research. Today is June 1, 2025. - -CORE RESPONSIBILITIES: -You help users create modern, well-structured applications by: -1. Understanding their requirements and suggesting appropriate technologies -2. Using Context7 tools to get the latest documentation and best practices -3. Creating complete, production-ready applications with proper project structure -4. Following modern development patterns and conventions -5. Providing beautiful, responsive UI/UX when building web applications - -CONTEXT7 TOOLS AVAILABLE: -You have access to Context7 tools for documentation and library research. The tools available depend on your setup: - -REAL CONTEXT7 TOOLS (when using @upstash/context7-mcp): -1. c41_resolve-library-id - Resolve library names to Context7-compatible library IDs -2. c41_get-library-docs - Fetch detailed, up-to-date documentation and code examples - -MOCK CONTEXT7 TOOLS (for local development/demo): -1. search_libraries - Search for libraries and frameworks by name or description -2. get_library_details - Get detailed information about a specific library -3. get_documentation - Fetch documentation and examples for a library - -CONTEXT7 WORKFLOW: -For Real Context7 Tools: -1. Use c41_resolve-library-id to get the correct library ID for any technology -2. Use c41_get-library-docs with the library ID to fetch comprehensive documentation - -For Mock Context7 Tools: -1. Use search_libraries to find relevant technologies and alternatives -2. Use get_library_details to get information about chosen libraries -3. Use get_documentation to fetch examples and best practices - -DEVELOPMENT GUIDELINES: -- Always use Context7 tools before implementing any technology -- Fetch comprehensive documentation before writing code -- Use the latest stable versions and best practices from retrieved documentation -- Create complete project structures with proper configuration -- Include proper error handling, validation, and testing setup -- For web apps, prioritize modern, responsive design with current UI patterns -- Use TypeScript for type safety when applicable -- Follow framework conventions (Next.js App Router, React best practices, etc.) -- Include proper dependency management and build scripts -- Reference actual code examples from the retrieved documentation - -CRITICAL NEXT.JS ROUTING RULES: -**NEVER create both app/ and pages/ directories in the same project** -- Use ONLY the App Router (app/ directory) for Next.js 13+ applications -- The App Router is the modern, recommended approach -- Creating both app/ and pages/ directories causes routing conflicts -- App Router structure: app/layout.tsx, app/page.tsx, app/about/page.tsx -- Pages Router is legacy and should not be used in new projects - -NEXT.JS APP ROUTER STRUCTURE (CORRECT): -Project should have app/ directory with layout.tsx and page.tsx files -- app/layout.tsx is the root layout (required) -- app/page.tsx is the homepage -- app/about/page.tsx would be the about page -- components/ directory for reusable components -- public/ directory for static assets - -NEVER CREATE CONFLICTING STRUCTURE: -Do not create both app/ and pages/ directories as this causes routing conflicts - -APP CREATION WORKFLOW: -When a user requests an application: -1. Clarify requirements and suggest appropriate technology stack -2. Use Context7 tools to research each major technology/library needed -3. Fetch comprehensive, current documentation for chosen technologies -4. Create complete project structure with proper configuration files -5. **For Next.js: Use ONLY App Router (app/ directory), never pages/ directory** -6. Implement core functionality using patterns from Context7 documentation -7. Add modern styling and responsive design -8. Include development scripts and build configuration -9. Provide setup, development, and deployment instructions -10. Include testing setup when appropriate - -SUPPORTED TECHNOLOGIES (always verify latest versions via Context7): -- Frontend: React, Next.js, Vue, Angular, Svelte, Vite -- Backend: Node.js, Express, Fastify, NestJS, Koa -- Databases: MongoDB, PostgreSQL, MySQL, SQLite, Redis -- Styling: Tailwind CSS, CSS Modules, Styled Components, Emotion -- Testing: Jest, Vitest, Playwright, Cypress, Testing Library -- Build Tools: Vite, Webpack, Rollup, Turbo -- Package Managers: npm, yarn, pnpm - -Always be thorough, use Context7 tools extensively for every technology involved, and create production-quality applications with current best practices.`; + return ` +You are an expert software development assistant with access to Context7 MCP tools for library documentation and research. Today is **June 1, 2025**. + +--- + +### 🔧 CORE RESPONSIBILITIES + +You help users create **modern, production-grade applications** by: + +1. Understanding user requirements and recommending the best-fit technologies +2. Using **Context7 tools** to retrieve up-to-date documentation and best practices +3. Building complete projects with proper structure and configuration +4. Following modern development conventions and patterns +5. Creating clean, responsive, and accessible UI/UX + +--- + +### 🧰 CONTEXT7 TOOLS + +You have access to either **Real** or **Mock** Context7 tools. + +**Real Context7 Tools (@upstash/context7-mcp):** + +* c41_resolve-library-id: Resolve technology names to Context7-compatible IDs +* c41_get-library-docs: Fetch full documentation, usage examples, and best practices + +**Mock Tools (for local/demo use):** + +* search_libraries: Search for libraries by name or functionality +* get_library_details: Fetch library metadata and features +* get_documentation: Fetch usage examples and implementation patterns + +--- + +### 📂 FILE SYSTEM RULES + +* All projects and generated files must **use the /tmp directory exclusively**. +* If a **Next.js project already exists in /tmp**, continue working within it instead of creating a new one. +* You must **never overwrite** an existing project unless explicitly asked. + +--- + +### ⚙️ DEVELOPMENT WORKFLOW + +**Always use Context7 tools before coding:** + +**Always list the files in a directory before creating new files.** + +1. Clarify requirements and tech stack +2. Lookup technologies using Context7 tools +3. Retrieve current documentation and patterns +4. Scaffold or enhance projects under /tmp, maintaining clean structure +5. Follow framework and language conventions +6. Include error handling, testing, and CI/build scripts +7. Prioritize maintainability, readability, and DX (developer experience) + +--- + +### ⚛️ NEXT.JS PROJECT RULES + +* **Use ONLY the App Router (/app)**, not the legacy Pages Router +* **Never create both (/app and /pages directories** +* Structure should include: + * app/layout.tsx – required root layout + * app/page.tsx - homepage + * app/about/page.tsx – nested routes + * components/, public/, etc. as needed + +If a Next.js project exists: + +* Validate it uses the App Router +* Extend or modify as needed based on the request + +--- + +### 🧪 TECH STACK (verify latest versions with Context7) + +**Frontend:** React, Next.js, Vue, Angular, Svelte +**Backend:** Node.js, Express, Fastify, NestJS, Koa +**Databases:** MongoDB, PostgreSQL, MySQL, SQLite, Redis +**Styling:** Tailwind CSS, CSS Modules, Styled Components +**Testing:** Jest, Vitest, Playwright, Cypress +**Build Tools:** Vite, Webpack, Rollup, Turbo +**Package Managers:** npm, yarn, pnpm + +--- + +### ✅ SUMMARY + +* Always work in /tmp +* If a project exists, enhance it — don't recreate +* Use Context7 tools for everything: tech decisions, patterns, and examples +* Adhere to modern best practices in project setup, UI/UX, and code quality +`; } async initialize(): Promise { @@ -148,69 +165,87 @@ Always be thorough, use Context7 tools extensively for every technology involved `🚀 Context7 Interactive Agent initialized using ${this.config.model} on ${this.config.provider}\n` ); - try { - // Health check - const isHealthy = await this.config.client.healthCheck(); - if (!isHealthy) { - console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); - process.exit(1); - } - - // Check if Context7 tools are available (real or mock) - const tools = await this.config.client.listTools(); - const realContext7Tools = tools.data.filter((tool) => - ['c41_resolve-library-id', 'c41_get-library-docs'].includes(tool.name) - ); - const mockContext7Tools = tools.data.filter((tool) => - [ - 'search_libraries', - 'get_library_details', - 'get_documentation', - ].includes(tool.name) - ); - - const context7Tools = [...realContext7Tools, ...mockContext7Tools]; + let attempt = 0; + while (attempt < this.config.maxRetries) { + try { + // Health check + const isHealthy = await this.config.client.healthCheck(); + if (!isHealthy) { + console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); + process.exit(1); + } - if (context7Tools.length === 0) { - console.error( - '⚠️ No Context7 MCP tools available. Make sure a Context7 MCP server is running.' - ); - console.error( - ' For real Context7: npx -y @upstash/context7-mcp@latest' + const tools = await this.config.client.listTools(); + const realContext7Tools = tools.data.filter((tool) => + ['c41_resolve-library-id', 'c41_get-library-docs'].includes(tool.name) ); - console.error( - ' For local mock: docker-compose up --build (already included in this project)' + const mockContext7Tools = tools.data.filter((tool) => + [ + 'search_libraries', + 'get_library_details', + 'get_documentation', + ].includes(tool.name) ); - process.exit(1); - } - const usingRealContext7 = realContext7Tools.length > 0; - const toolType = usingRealContext7 ? 'real Context7' : 'mock Context7'; + const context7Tools = [...realContext7Tools, ...mockContext7Tools]; + + if (context7Tools.length === 0) { + console.error( + '⚠️ No Context7 MCP tools available. Make sure a Context7 MCP server is running.' + ); + console.error( + ' For real Context7: npx -y @upstash/context7-mcp@latest' + ); + console.error( + ' For local mock: docker-compose up --build (already included in this project)' + ); + process.exit(1); + } - console.info( - `📋 Found ${context7Tools.length} ${toolType} tools available:` - ); - context7Tools.forEach((tool, index) => { - console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); - }); + const usingRealContext7 = realContext7Tools.length > 0; + const toolType = usingRealContext7 ? 'real Context7' : 'mock Context7'; - if (!usingRealContext7) { - console.info('💡 Using local mock Context7 server for demonstration'); console.info( - ' To use real Context7 with latest docs, install: npx -y @upstash/context7-mcp@latest' + `📋 Found ${context7Tools.length} ${toolType} tools available:` + ); + context7Tools.forEach((tool, index) => { + console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); + }); + + if (!usingRealContext7) { + console.info('💡 Using local mock Context7 server for demonstration'); + console.info( + ' To use real Context7 with latest docs, install: npx -y @upstash/context7-mcp@latest' + ); + } + console.info(''); + + this.showWelcomeMessage(); + await this.startInteractiveSession(); + break; // Success, exit retry loop + } catch (error) { + attempt++; + console.error( + `❌ Initialization Error (attempt ${attempt}/${this.config.maxRetries}):`, + (error as Error).message ); + + if (attempt < this.config.maxRetries) { + console.log( + `⏳ Retrying in ${this.config.retryDelayMs / 1000} seconds...` + ); + await this.delay(this.config.retryDelayMs); + } else { + console.error( + `❌ Failed to initialize after ${this.config.maxRetries} attempts` + ); + console.log( + '\n💡 Make sure the Context7 MCP server is running on port 3002' + ); + console.log(' and the Inference Gateway is running on port 8080'); + process.exit(1); + } } - console.info(''); - - this.showWelcomeMessage(); - await this.startInteractiveSession(); - } catch (error) { - console.error('❌ Initialization Error:', (error as Error).message); - console.log( - '\n💡 Make sure the Context7 MCP server is running on port 3002' - ); - console.log(' and the Inference Gateway is running on port 8080'); - process.exit(1); } } @@ -257,7 +292,7 @@ Always be thorough, use Context7 tools extensively for every technology involved } if (userInput.trim()) { - await this.processUserRequest(userInput); + await this.processUserRequestWithRetry(userInput); } } } @@ -300,6 +335,36 @@ Always be thorough, use Context7 tools extensively for every technology involved } } + private async processUserRequestWithRetry(userInput: string): Promise { + let attempt = 0; + + while (attempt < this.config.maxRetries) { + try { + await this.processUserRequest(userInput); + break; // Success, exit retry loop + } catch (error) { + attempt++; + console.error( + `❌ Request failed (attempt ${attempt}/${this.config.maxRetries}):`, + (error as Error).message + ); + + if (attempt < this.config.maxRetries) { + console.log( + `⏳ Retrying in ${this.config.retryDelayMs / 1000} seconds...` + ); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + await this.delay(this.config.retryDelayMs); + } else { + console.error( + `❌ Failed to process request after ${this.config.maxRetries} attempts` + ); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + } + } + } + } + private async processUserRequest(userInput: string): Promise { console.log(`\n🔍 Processing request: "${userInput}"`); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); @@ -310,59 +375,53 @@ Always be thorough, use Context7 tools extensively for every technology involved content: userInput, }); - try { - let assistantResponse = ''; - - await this.config.client.streamChatCompletion( - { - model: `${this.config.provider}/${this.config.model}`, - messages: this.config.conversationHistory, - max_tokens: 2000, + let assistantResponse = ''; + + await this.config.client.streamChatCompletion( + { + model: `${this.config.provider}/${this.config.model}`, + messages: this.config.conversationHistory, + max_tokens: 2000, + }, + { + onOpen: () => { + console.log('🔗 Starting development session with Context7...\n'); }, - { - onOpen: () => { - console.log('🔗 Starting development session with Context7...\n'); - }, - onReasoning: (reasoning) => { - console.log(`\n🤔 Agent Reasoning: ${reasoning}`); - }, - onContent: (content) => { - process.stdout.write(content); - assistantResponse += content; - }, - onMCPTool: (toolCall) => { - console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); - try { - const args = JSON.parse(toolCall.function.arguments); - console.log(`📝 Arguments:`, JSON.stringify(args, null, 2)); - } catch { - console.log(`📝 Raw Arguments: ${toolCall.function.arguments}`); - } - console.log(`🔍 Tool ID: ${toolCall.id}\n`); - }, - onError: (error) => { - console.error(`\n❌ Stream Error: ${error.error}`); - }, - onFinish: () => { - console.log('\n\n✅ Development session completed!\n'); - console.log( - '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' - ); - - // Add assistant response to conversation history - if (assistantResponse.trim()) { - this.config.conversationHistory.push({ - role: MessageRole.assistant, - content: assistantResponse, - }); - } - }, - } - ); - } catch (error) { - console.error('\n❌ Error processing request:', (error as Error).message); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - } + onReasoning: (reasoning) => { + console.log(`\n🤔 Agent Reasoning: ${reasoning}`); + }, + onContent: (content) => { + process.stdout.write(content); + assistantResponse += content; + }, + onMCPTool: (toolCall) => { + console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); + try { + const args = JSON.parse(toolCall.function.arguments); + console.log(`📝 Arguments:`, JSON.stringify(args, null, 2)); + } catch { + console.log(`📝 Raw Arguments: ${toolCall.function.arguments}`); + } + console.log(`🔍 Tool ID: ${toolCall.id}\n`); + }, + onError: (error) => { + console.error(`\n❌ Stream Error: ${error.error}`); + throw new Error(`Stream error: ${error.error}`); + }, + onFinish: () => { + console.log('\n\n✅ Development session completed!\n'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + // Add assistant response to conversation history + if (assistantResponse.trim()) { + this.config.conversationHistory.push({ + role: MessageRole.assistant, + content: assistantResponse, + }); + } + }, + } + ); } async shutdown(): Promise { From 07e5fa4afbc17e46f4ade0f00035e24005f3612f Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 11:48:13 +0000 Subject: [PATCH 14/38] docs(example-agent): Add wait functionality for Next.js project creation and update system prompt Signed-off-by: Eden Reich --- examples/mcp/example-agent.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/examples/mcp/example-agent.ts b/examples/mcp/example-agent.ts index e7a4b63..ef1878a 100644 --- a/examples/mcp/example-agent.ts +++ b/examples/mcp/example-agent.ts @@ -63,6 +63,14 @@ class Context7Agent { }); } + private async waitForProjectCreation(): Promise { + console.log( + '⏳ Waiting 30 seconds for Next.js project creation to complete...' + ); + await this.delay(30000); + console.log('✅ Project creation wait period completed.\n'); + } + private getSystemPrompt(): string { return ` You are an expert software development assistant with access to Context7 MCP tools for library documentation and research. Today is **June 1, 2025**. @@ -112,6 +120,8 @@ You have access to either **Real** or **Mock** Context7 tools. **Always list the files in a directory before creating new files.** +**When creating a Next.js project, always wait 30 seconds after project creation.** + 1. Clarify requirements and tech stack 2. Lookup technologies using Context7 tools 3. Retrieve current documentation and patterns @@ -126,6 +136,7 @@ You have access to either **Real** or **Mock** Context7 tools. * **Use ONLY the App Router (/app)**, not the legacy Pages Router * **Never create both (/app and /pages directories** +* **IMPORTANT: Always wait 30 seconds after creating a Next.js project before proceeding** * Structure should include: * app/layout.tsx – required root layout * app/page.tsx - homepage @@ -376,6 +387,7 @@ If a Next.js project exists: }); let assistantResponse = ''; + let shouldWaitForProject = false; await this.config.client.streamChatCompletion( { @@ -403,15 +415,31 @@ If a Next.js project exists: console.log(`📝 Raw Arguments: ${toolCall.function.arguments}`); } console.log(`🔍 Tool ID: ${toolCall.id}\n`); + + if ( + toolCall.function.name === 'create_next_project' || + toolCall.function.name === 'create_nextjs_project' || + toolCall.function.name === 'create_new_workspace' || + toolCall.function.name.toLowerCase().includes('next') + ) { + console.log( + '🎯 Next.js project creation detected - will wait 30 seconds after completion' + ); + shouldWaitForProject = true; + } }, onError: (error) => { console.error(`\n❌ Stream Error: ${error.error}`); throw new Error(`Stream error: ${error.error}`); }, - onFinish: () => { + onFinish: async () => { console.log('\n\n✅ Development session completed!\n'); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + if (shouldWaitForProject) { + await this.waitForProjectCreation(); + } + // Add assistant response to conversation history if (assistantResponse.trim()) { this.config.conversationHistory.push({ From bcd9a935555d60cc993f2860c7a0d87926af6b7b Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 12:09:16 +0000 Subject: [PATCH 15/38] docs(examples): Remove outdated examples and add Vite agent for modern application development - Deleted example-npm.ts and example-tool-demo.ts as they are no longer needed. - Introduced example-vite-agent.ts to facilitate the creation of Vite-based applications. - Updated package.json to reflect the removal of old examples and the addition of the new Vite agent. - Cleaned up filesystem server code by removing unnecessary comments and simplifying allowed directories. - Removed sample_sales_data.csv as it is no longer relevant to the current examples. Signed-off-by: Eden Reich --- examples/mcp/README.md | 144 +++-- examples/mcp/example-advanced.ts | 261 --------- examples/mcp/example-basic.ts | 130 ----- examples/mcp/example-debug-args.ts | 66 --- examples/mcp/example-handler-demo.ts | 196 ------- examples/mcp/example-kubernetes-agent.ts | 538 ++++++++++++++++++ examples/mcp/example-list-tools.ts | 29 - examples/mcp/example-mcp-tools.ts | 207 ------- ...ample-agent.ts => example-nextjs-agent.ts} | 11 +- examples/mcp/example-npm.ts | 194 ------- examples/mcp/example-tool-demo.ts | 221 ------- examples/mcp/example-vite-agent.ts | 505 ++++++++++++++++ examples/mcp/mcp-servers/filesystem/index.js | 91 +-- examples/mcp/package.json | 14 +- examples/mcp/shared/sample_sales_data.csv | 16 - 15 files changed, 1142 insertions(+), 1481 deletions(-) delete mode 100644 examples/mcp/example-advanced.ts delete mode 100644 examples/mcp/example-basic.ts delete mode 100644 examples/mcp/example-debug-args.ts delete mode 100644 examples/mcp/example-handler-demo.ts create mode 100644 examples/mcp/example-kubernetes-agent.ts delete mode 100644 examples/mcp/example-list-tools.ts delete mode 100644 examples/mcp/example-mcp-tools.ts rename examples/mcp/{example-agent.ts => example-nextjs-agent.ts} (97%) delete mode 100644 examples/mcp/example-npm.ts delete mode 100644 examples/mcp/example-tool-demo.ts create mode 100644 examples/mcp/example-vite-agent.ts delete mode 100644 examples/mcp/shared/sample_sales_data.csv diff --git a/examples/mcp/README.md b/examples/mcp/README.md index db93e09..55d6eb2 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -2,47 +2,17 @@ This directory contains comprehensive examples demonstrating how to use the Inference Gateway SDK with Model Context Protocol (MCP) tools in a multi-provider architecture. Each example showcases different aspects of MCP tool integration. -## 📁 Available Examples - -### Core Examples - -- **`example-basic.ts`** - Basic MCP tool usage and file operations -- **`example-advanced.ts`** - Multi-scenario demonstration with complex workflows -- **`example-nextjs.ts`** - Next.js application generator with documentation fetching -- **`example-handler-demo.ts`** - Comprehensive `onMCPTool` handler demonstration - -### Debugging & Testing Examples - -- **`example-tool-demo.ts`** - Tool argument analysis and issue identification -- **`example-debug-args.ts`** - Raw tool argument debugging -- **`example-list-tools.ts`** - List all available MCP tools with schemas -- **`example-mcp-tools.ts`** - Legacy MCP tools example - ## 🚀 Quick Start ### Run Specific Examples ```bash -# Run specific examples by name -npm run example:basic -npm run example:advanced -npm run example:nextjs -npm run example:tool-demo -npm run example:handler-demo -npm run example:debug-args -npm run example:list-tools -``` - -## Features Demonstrated -1. **MCP Tool Discovery** - List and explore available MCP tools -2. **File Operations** - Use filesystem MCP server for file operations -3. **Web Scraping** - Fetch content from URLs using MCP tools -4. **Multi-Tool Conversations** - Combine multiple MCP tools in single conversations -5. **Tool Function Calling** - Stream responses with real-time tool execution -6. **Data Analysis** - Analyze sample data files with AI assistance -7. **Enhanced Tool Debugging** - Comprehensive `onMCPTool` handler examples -8. **Error Handling** - Robust handling of incomplete tool arguments and failures +# Run interactive specialized agents +npm run example:nextjs-agent # 🤖 Next.js development agent +npm run example:vite-agent # ⚡ Vite application agent +npm run example:kubernetes-agent # ☸️ Kubernetes operations agent +``` ## Architecture @@ -57,18 +27,94 @@ This example uses Docker Compose to orchestrate: The MCP filesystem server is configured with restricted access for security: -- **`/shared`** - Read-only directory with sample data files -- **`/tmp`** - Read-write directory for temporary files +- **`/shared`** - Read-Write directory for shared files between the host and container +- **`/tmp`** - Read-write directory for temporary files inside of the container The AI will only be able to access these directories. This prevents unauthorized access to system files. ## Sample Data -The `/shared` directory contains example files for testing: +The `/shared` directory contains example files for testing. + +## 🤖 Interactive Development Agents + +This repository includes three specialized interactive agents that use Context7 MCP tools for up-to-date documentation and best practices: + +### ⚡ Vite Agent (`example-vite-agent.ts`) + +**Purpose:** Creates lightning-fast modern Vite applications with optimal configuration. + +**Specializes in:** + +- React, Vue, Svelte, and vanilla JavaScript/TypeScript projects +- Modern build tooling and optimization +- Fast development server with HMR +- Vitest testing setup +- TypeScript configuration +- CSS preprocessing (Tailwind, Sass, PostCSS) + +**Run with:** `npm run example:vite-agent` + +**Example requests:** + +- "Create a React + TypeScript app with Vite and Tailwind CSS" +- "Build a Vue 3 dashboard with Vite, Vitest, and component library" +- "Make a Svelte SPA with Vite and optimal build configuration" + +### ☸️ Kubernetes Agent (`example-kubernetes-agent.ts`) + +**Purpose:** Handles Kubernetes cluster operations and container orchestration. + +**Specializes in:** + +- Production-ready YAML manifests +- Deployment strategies (blue-green, canary) +- RBAC and security policies +- Service mesh configuration +- Monitoring and observability +- Scaling and resource management +- CI/CD pipeline integration + +**Run with:** `npm run example:kubernetes-agent` + +**Example requests:** + +- "Deploy a scalable web application with load balancing and auto-scaling" +- "Create a microservices architecture with service mesh and monitoring" +- "Set up RBAC and network policies for multi-tenant cluster" +- "Configure GitOps deployment pipeline with ArgoCD" + +### 🤖 Next.js Agent (`example-nextjs-agent.ts`) + +**Purpose:** Creates modern Next.js applications with App Router and latest features. + +**Specializes in:** + +- Next.js 13+ App Router architecture +- Server Components and streaming +- TypeScript and modern tooling +- Performance optimization +- SEO and accessibility +- Production deployment + +**Run with:** `npm run example:nextjs-agent` + +**Example requests:** + +- "Create a Next.js blog with TypeScript and Tailwind CSS" +- "Build a React dashboard with charts and data visualization" +- "Make a full-stack Next.js app with authentication and database" + +### 🧰 Context7 Integration + +All agents use Context7 MCP tools to: + +- **Fetch Latest Documentation** - Get current best practices and API references +- **Resolve Library Dependencies** - Find compatible package versions +- **Access Code Examples** - Retrieve real-world implementation patterns +- **Stay Current** - Use up-to-date information instead of static training data -- `mcp-filesystem-example.txt` - Basic example file -- `sample_sales_data.csv` - Sales data for analysis exercises -- `README.md` - Documentation about available files +Each agent maintains conversation history and provides interactive help. Use `clear` to reset, `help` for guidance, or `exit` to quit. ## Setup Instructions @@ -200,22 +246,12 @@ npx tsx filesystem-demo.ts ## Available Examples -- `index.ts` - Enhanced MCP demonstration with comprehensive tool handling -- `advanced-example.ts` - Multi-scenario examples showing file operations, web content, and data analysis -- `nextjs-example.ts` - **IMPROVED!** Create a complete Next.js app with enhanced error handling and URL parsing -- `handler-demo.ts` - Focused demonstration of the onMCPTool handler with detailed metrics and logging -- `tool-demo.ts` - **NEW!** Comprehensive tool demonstration that identifies schema issues -- `list-tools.ts` - **NEW!** Inspect MCP tool schemas and parameters -- `example-mcp-tools.ts` - Basic MCP tool discovery and testing +- `npm run example:nextjs-agent` - Next.js application generator +- `npm run example:vite-agent` - Vite application generator +- `npm run example:kubernetes-agent` - Kubernetes deployment example ## Available Commands -- `npm start` - Run the main enhanced MCP example (index.ts) -- `npm run advanced` - Run the advanced multi-scenario examples -- `npm run nextjs` - Run improved Next.js app creator with better debugging -- `npm run handler-demo` - Run the focused MCP tool handler demonstration -- `npx tsx tool-demo.ts` - **NEW!** Run comprehensive tool testing and issue identification -- `npx tsx list-tools.ts` - **NEW!** List all MCP tools with detailed schemas - `npm run compose:up` - Start all services in background - `npm run compose:down` - Stop all services - `npm run compose:logs` - View logs from all services diff --git a/examples/mcp/example-advanced.ts b/examples/mcp/example-advanced.ts deleted file mode 100644 index 129ea7d..0000000 --- a/examples/mcp/example-advanced.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { - InferenceGatewayClient, - MessageRole, - Provider, -} from '../../src/index.js'; - -(async () => { - const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', - }); - - const provider = (process.env.PROVIDER as Provider) || Provider.groq; - const model = process.env.LLM || 'llama-3.3-70b-versatile'; - - console.info(`🚀 Advanced MCP Demo using ${model} on ${provider}\n`); - - try { - // Health check - const isHealthy = await client.healthCheck(); - if (!isHealthy) { - console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); - process.exit(1); - } - - // Discover available MCP tools - const tools = await client.listTools(); - console.info(`📋 Found ${tools.data.length} MCP tools available:`); - tools.data.forEach((tool, index) => { - console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); - }); - console.info(''); - - if (tools.data.length === 0) { - console.error( - '⚠️ No MCP tools available. Check MCP server configuration.' - ); - return; - } - - // Example 1: File Operations Chain - console.info('=== Example 1: File Operations Chain ===\n'); - await runFileOperationsExample(client, model, provider); - - console.info('\n' + '='.repeat(50) + '\n'); - - // Example 2: Web Content Analysis - console.info('=== Example 2: Web Content Analysis ===\n'); - await runWebContentExample(client, model, provider); - - console.info('\n' + '='.repeat(50) + '\n'); - - // Example 3: Data Analysis with CSV - console.info('=== Example 3: Data Analysis ===\n'); - await runDataAnalysisExample(client, model, provider); - } catch (error) { - if ( - error instanceof Error && - error.message.includes('MCP tools endpoint is not exposed') - ) { - console.error( - '❌ MCP not exposed. Set EXPOSE_MCP=true and restart gateway.' - ); - } else { - console.error('❌ Error:', error); - } - } -})(); - -async function runFileOperationsExample( - client: InferenceGatewayClient, - model: string, - provider: Provider -) { - let toolCallCount = 0; - let contentBuffer = ''; - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a file management assistant. Use the available MCP tools to: -1. Create a configuration file at /tmp/config.json with sample data -2. Read the file back to verify -3. List the /tmp directory to show what's there -4. Provide a summary of what you accomplished - -Be detailed about each step and confirm successful operations.`, - }, - { - role: MessageRole.user, - content: - 'Help me set up a sample configuration file with some JSON data.', - }, - ], - max_tokens: 1000, - }, - { - onContent: (content) => { - process.stdout.write(content); - contentBuffer += content; - }, - onMCPTool: (toolCall) => { - toolCallCount++; - console.info( - `\n🛠️ [${toolCallCount}] MCP Tool: ${toolCall.function.name}` - ); - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); - } catch { - console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); - } - console.info(`🔍 Tool ID: ${toolCall.id}`); - console.info(''); // Add spacing before next content - }, - onUsageMetrics: (usage) => { - console.info( - `\n📊 Tokens - Prompt: ${usage.prompt_tokens}, Completion: ${usage.completion_tokens}, Total: ${usage.total_tokens}` - ); - }, - onFinish: () => { - console.info( - `\n✅ File operations completed! Used ${toolCallCount} MCP tools.\n` - ); - }, - onError: (error) => { - console.error('\n❌ Stream Error:', error); - }, - }, - provider - ); -} - -async function runWebContentExample( - client: InferenceGatewayClient, - model: string, - provider: Provider -) { - let toolCallCount = 0; - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a web research assistant. Use the fetch_url tool to get content from URLs and analyze it. -Provide summaries and key insights from the content you retrieve.`, - }, - { - role: MessageRole.user, - content: - 'Can you fetch the content from https://httpbin.org/json and tell me what information it contains?', - }, - ], - max_tokens: 800, - }, - { - onContent: (content) => { - process.stdout.write(content); - }, - onMCPTool: (toolCall) => { - toolCallCount++; - console.info( - `\n🌐 [${toolCallCount}] Web Tool: ${toolCall.function.name}` - ); - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`🔗 URL: ${args.url || 'N/A'}`); - if (args.timeout) { - console.info(`⏱️ Timeout: ${args.timeout}ms`); - } - } catch { - console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); - } - console.info(`🔍 Tool ID: ${toolCall.id}`); - console.info(''); // Add spacing - }, - onFinish: () => { - console.info( - `\n✅ Web content analysis completed! Used ${toolCallCount} web tools.\n` - ); - }, - onError: (error) => { - console.error('\n❌ Stream Error:', error); - }, - }, - provider - ); -} - -async function runDataAnalysisExample( - client: InferenceGatewayClient, - model: string, - provider: Provider -) { - let toolCallCount = 0; - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a data analyst. Use the available MCP tools to: -1. Read the CSV file at /shared/sample_sales_data.csv -2. Analyze the data structure and content -3. Provide insights about the data -4. Create a summary report and save it to /tmp/analysis_report.txt - -Be thorough in your analysis and explanations.`, - }, - { - role: MessageRole.user, - content: - 'Please analyze the sample sales data and create a comprehensive report.', - }, - ], - max_tokens: 1200, - }, - { - onContent: (content) => { - process.stdout.write(content); - }, - onMCPTool: (toolCall) => { - toolCallCount++; - console.info( - `\n📊 [${toolCallCount}] Analysis Tool: ${toolCall.function.name}` - ); - try { - const args = JSON.parse(toolCall.function.arguments); - if (args.file_path) { - console.info(`📁 File: ${args.file_path}`); - } - if (args.content && args.content.length > 100) { - console.info( - `📝 Content: ${args.content.substring(0, 100)}... (${args.content.length} chars)` - ); - } else if (args.content) { - console.info(`📝 Content: ${args.content}`); - } - } catch { - console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); - } - console.info(`🔍 Tool ID: ${toolCall.id}`); - console.info(''); // Add spacing - }, - onFinish: () => { - console.info( - `\n✅ Data analysis completed! Used ${toolCallCount} analysis tools.\n` - ); - }, - onError: (error) => { - console.error('\n❌ Stream Error:', error); - }, - }, - provider - ); -} diff --git a/examples/mcp/example-basic.ts b/examples/mcp/example-basic.ts deleted file mode 100644 index adf3d82..0000000 --- a/examples/mcp/example-basic.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - InferenceGatewayClient, - MessageRole, - Provider, -} from '../../src/index.js'; - -(async () => { - const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', - }); - - const provider = (process.env.PROVIDER as Provider) || Provider.groq; - const model = process.env.LLM || 'llama-3.3-70b-versatile'; - - console.info(`🚀 Using ${model} on ${provider}\n`); - - try { - // Health check - const isHealthy = await client.healthCheck(); - if (!isHealthy) { - console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); - process.exit(1); - } - - // Discover available MCP tools - const tools = await client.listTools(); - console.info(`📋 Found ${tools.data.length} MCP tools available\n`); - - if (tools.data.length === 0) { - console.error( - '⚠️ No MCP tools available. Check MCP server configuration.' - ); - return; - } - - // Display available tools - console.info('📋 Available MCP Tools:'); - tools.data.forEach((tool, index) => { - console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); - }); - console.info(''); - - // Comprehensive MCP tool demonstration - console.info('=== MCP Tool Demo ===\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a helpful assistant with access to MCP tools for file operations and web content fetching. - -IMPORTANT: You MUST use the available tools to complete tasks. When asked to work with files: -1. Use write_file to create files -2. Use read_file to read content -3. Use list_directory to explore directories -4. Always confirm what you did - -For web content: -1. Use fetch_url to get content from URLs -2. Summarize and analyze the content - -Be verbose about your tool usage and explain what you're doing step by step.`, - }, - { - role: MessageRole.user, - content: - 'Please write "Hello MCP Tools!" to /tmp/demo.txt, then read it back to confirm, and finally list the /tmp directory contents.', - }, - ], - max_tokens: 800, - }, - { - onOpen: () => { - console.info('🔗 Connection opened, starting stream...\n'); - }, - onContent: (content) => { - process.stdout.write(content); - }, - onMCPTool: (toolCall) => { - console.info(`\n🛠️ MCP Tool Called: ${toolCall.function.name}`); - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); - } catch { - console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); - } - console.info(`🔍 Tool ID: ${toolCall.id}\n`); - }, - onTool: (toolCall) => { - // This would handle regular (non-MCP) tools if any were provided - console.info(`\n🔧 Regular Tool Called: ${toolCall.function.name}`); - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); - } catch { - console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); - } - console.info(`🔍 Tool ID: ${toolCall.id}\n`); - }, - onUsageMetrics: (usage) => { - console.info(`\n📊 Token Usage:`, { - prompt: usage.prompt_tokens, - completion: usage.completion_tokens, - total: usage.total_tokens, - }); - }, - onFinish: () => { - console.info('\n\n✅ Demo completed successfully!\n'); - }, - onError: (error) => { - console.error('\n❌ Stream Error:', error); - }, - }, - provider - ); - } catch (error) { - if ( - error instanceof Error && - error.message.includes('MCP tools endpoint is not exposed') - ) { - console.error( - '❌ MCP not exposed. Set EXPOSE_MCP=true and restart gateway.' - ); - } else { - console.error('❌ Error:', error); - } - } -})(); diff --git a/examples/mcp/example-debug-args.ts b/examples/mcp/example-debug-args.ts deleted file mode 100644 index e82c957..0000000 --- a/examples/mcp/example-debug-args.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as dotenv from 'dotenv'; -import { - InferenceGatewayClient, - MessageRole, - Provider, -} from '../../src/index.js'; - -dotenv.config(); - -(async () => { - const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', - }); - - const provider = (process.env.PROVIDER as Provider) || Provider.openai; - const model = process.env.LLM || 'gpt-4o'; - - console.info(`🧪 Testing MCP Tool Arguments - ${model} on ${provider}\n`); - - try { - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.user, - content: - 'Please fetch the content from https://nextjs.org/docs and show me what you find.', - }, - ], - max_tokens: 500, - }, - { - onMCPTool: (toolCall) => { - console.info(`\n🔧 MCP Tool Called: ${toolCall.function.name}`); - console.info(`🆔 Tool ID: ${toolCall.id}`); - console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); - - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`✅ Parsed Arguments:`, JSON.stringify(args, null, 2)); - - // Show all available properties - const keys = Object.keys(args); - console.info(`🔑 Available properties: ${keys.join(', ')}`); - } catch (e) { - console.info(`❌ JSON Parse Error: ${e.message}`); - } - console.info(''); // Add spacing - }, - onContent: (content) => { - process.stdout.write(content); - }, - onFinish: () => { - console.info(`\n\n✅ Test completed!\n`); - }, - onError: (error) => { - console.error('\n❌ Error:', error); - }, - }, - provider - ); - } catch (error) { - console.error('❌ Error:', error); - } -})(); diff --git a/examples/mcp/example-handler-demo.ts b/examples/mcp/example-handler-demo.ts deleted file mode 100644 index 1e0f50c..0000000 --- a/examples/mcp/example-handler-demo.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { - InferenceGatewayClient, - MessageRole, - Provider, -} from '../../src/index.js'; - -/** - * This example specifically demonstrates the enhanced MCP tool handler features - * including the onMCPTool callback with detailed logging and tracking. - */ -(async () => { - const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', - }); - - const provider = (process.env.PROVIDER as Provider) || Provider.groq; - const model = process.env.LLM || 'llama-3.3-70b-versatile'; - - console.info(`🎯 MCP Tool Handler Demo using ${model} on ${provider}\n`); - - try { - // Health check - const isHealthy = await client.healthCheck(); - if (!isHealthy) { - console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); - process.exit(1); - } - - // Track various metrics - let totalToolCalls = 0; - let mcpToolCalls = 0; - let regularToolCalls = 0; - const toolCallTimestamps: Array<{ - tool: string; - timestamp: Date; - id: string; - }> = []; - - console.info('=== Enhanced MCP Tool Handler Demo ===\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a helpful assistant demonstrating MCP tool usage. - -TASK: Perform the following operations step by step: -1. Create a demo file at /tmp/handler-test.txt with "MCP Handler Demo" content -2. Read the file back to verify -3. List the /tmp directory -4. Create a simple JSON file at /tmp/demo-config.json with some configuration data -5. Read that JSON file back - -Be explicit about each step and confirm completion.`, - }, - { - role: MessageRole.user, - content: - 'Please demonstrate MCP tool usage by performing the file operations listed in the system prompt.', - }, - ], - max_tokens: 1000, - }, - { - onOpen: () => { - console.info('🔌 Stream connection opened\n'); - }, - onContent: (content) => { - // Use a subtle indicator for content - process.stdout.write(content); - }, - onMCPTool: (toolCall) => { - mcpToolCalls++; - totalToolCalls++; - const timestamp = new Date(); - toolCallTimestamps.push({ - tool: toolCall.function.name, - timestamp, - id: toolCall.id, - }); - - console.info( - `\n🛠️ MCP Tool #${mcpToolCalls}: ${toolCall.function.name}` - ); - console.info(`🆔 Tool ID: ${toolCall.id}`); - console.info(`⏰ Called at: ${timestamp.toISOString()}`); - - try { - const args = JSON.parse(toolCall.function.arguments); - - // Format arguments nicely based on tool type - if (toolCall.function.name === 'write_file') { - console.info(`📁 File: ${args.path}`); - if (args.content && args.content.length > 50) { - console.info( - `📝 Content: ${args.content.substring(0, 50)}... (${args.content.length} chars)` - ); - } else { - console.info(`📝 Content: ${args.content}`); - } - console.info(`🖥️ Server: ${args.mcpServer}`); - } else if (toolCall.function.name === 'read_file') { - console.info(`📖 Reading: ${args.path}`); - console.info(`🖥️ Server: ${args.mcpServer}`); - } else if (toolCall.function.name === 'list_directory') { - console.info(`📂 Listing: ${args.path}`); - console.info(`🖥️ Server: ${args.mcpServer}`); - } else { - console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); - } - } catch (parseError) { - console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); - console.warn(`⚠️ Failed to parse arguments: ${parseError}`); - } - - console.info(''); // Add spacing for readability - }, - onTool: (toolCall) => { - // This handles regular (non-MCP) tools - regularToolCalls++; - totalToolCalls++; - console.info(`\n🔧 Regular Tool Called: ${toolCall.function.name}`); - console.info(`🆔 Tool ID: ${toolCall.id}`); - console.info(`📝 Arguments: ${toolCall.function.arguments}\n`); - }, - onUsageMetrics: (usage) => { - console.info(`\n📊 Token Metrics:`); - console.info(` Prompt Tokens: ${usage.prompt_tokens}`); - console.info(` Completion Tokens: ${usage.completion_tokens}`); - console.info(` Total Tokens: ${usage.total_tokens}`); - if (usage.prompt_tokens && usage.completion_tokens) { - const efficiency = ( - (usage.completion_tokens / usage.prompt_tokens) * - 100 - ).toFixed(1); - console.info( - ` Efficiency: ${efficiency}% (completion/prompt ratio)` - ); - } - }, - onFinish: () => { - console.info('\n' + '='.repeat(60)); - console.info('📈 Session Summary:'); - console.info(` Total Tool Calls: ${totalToolCalls}`); - console.info(` MCP Tool Calls: ${mcpToolCalls}`); - console.info(` Regular Tool Calls: ${regularToolCalls}`); - - if (toolCallTimestamps.length > 0) { - console.info('\n🕒 Tool Call Timeline:'); - toolCallTimestamps.forEach((call, index) => { - const timeStr = call.timestamp.toLocaleTimeString(); - console.info( - ` ${index + 1}. ${timeStr} - ${call.tool} (${call.id.substring(0, 8)}...)` - ); - }); - - // Calculate duration between first and last tool call - if (toolCallTimestamps.length > 1) { - const duration = - toolCallTimestamps[ - toolCallTimestamps.length - 1 - ].timestamp.getTime() - - toolCallTimestamps[0].timestamp.getTime(); - console.info(`\n⏱️ Total tool execution span: ${duration}ms`); - } - } - - console.info('\n✅ MCP Tool Handler Demo completed successfully!'); - console.info('='.repeat(60) + '\n'); - }, - onError: (error) => { - console.error('\n❌ Stream Error occurred:'); - console.error(' Error:', error); - console.error(` At: ${new Date().toISOString()}`); - console.error( - ` Total tools called before error: ${totalToolCalls}\n` - ); - }, - }, - provider - ); - } catch (error) { - if ( - error instanceof Error && - error.message.includes('MCP tools endpoint is not exposed') - ) { - console.error( - '❌ MCP not exposed. Set EXPOSE_MCP=true and restart gateway.' - ); - } else { - console.error('❌ Unexpected error:', error); - } - } -})(); diff --git a/examples/mcp/example-kubernetes-agent.ts b/examples/mcp/example-kubernetes-agent.ts new file mode 100644 index 0000000..2cb0093 --- /dev/null +++ b/examples/mcp/example-kubernetes-agent.ts @@ -0,0 +1,538 @@ +/** + * Interactive Kubernetes Operations Agent + * + * This agent specializes in Kubernetes cluster management, deployment automation, + * and container orchestration using Context7 MCP tools for up-to-date K8s documentation. + */ + +import * as dotenv from 'dotenv'; +import * as path from 'path'; +import * as readline from 'readline'; +import { + InferenceGatewayClient, + MessageRole, + Provider, +} from '../../src/index.js'; + +dotenv.config({ path: path.join(__dirname, '.env') }); + +declare const require: any; +declare const module: any; + +interface AgentConfig { + client: InferenceGatewayClient; + provider: Provider; + model: string; + conversationHistory: Array<{ role: MessageRole; content: string }>; + maxRetries: number; + retryDelayMs: number; +} + +class KubernetesAgent { + private config: AgentConfig; + private rl: readline.Interface; + + constructor() { + this.config = { + client: new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }), + provider: (process.env.PROVIDER as Provider) || Provider.groq, + model: process.env.LLM || 'llama-3.3-70b-versatile', + conversationHistory: [], + maxRetries: 3, + retryDelayMs: 60000, + }; + + this.rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + this.config.conversationHistory.push({ + role: MessageRole.system, + content: this.getSystemPrompt(), + }); + } + + private async delay(ms: number): Promise { + return new Promise((resolve) => { + global.setTimeout(() => resolve(), ms); + }); + } + + private async waitForKubernetesOperation(): Promise { + console.log( + '⏳ Waiting 10 seconds for Kubernetes operation to complete...' + ); + await this.delay(10000); + console.log('✅ Kubernetes operation wait period completed.\n'); + } + + private getSystemPrompt(): string { + return ` +You are an expert Kubernetes operations assistant with access to Context7 MCP tools for K8s documentation and research. Today is **June 1, 2025**. + +--- + +### 🔧 CORE RESPONSIBILITIES + +You help users with **Kubernetes cluster operations and container orchestration** by: + +1. Understanding deployment requirements and recommending optimal K8s strategies +2. Using **Context7 tools** to retrieve up-to-date Kubernetes documentation and best practices +3. Creating production-ready YAML manifests and Helm charts +4. Following Kubernetes security and performance conventions +5. Providing cluster management, monitoring, and troubleshooting guidance + +--- + +### 🧰 CONTEXT7 TOOLS + +You have access to either **Real** or **Mock** Context7 tools. + +**Real Context7 Tools (@upstash/context7-mcp):** + +* c41_resolve-library-id: Resolve technology names to Context7-compatible IDs +* c41_get-library-docs: Fetch full documentation, usage examples, and best practices + + +* search_libraries: Search for libraries by name or functionality +* get_library_details: Fetch library metadata and features +* get_documentation: Fetch usage examples and implementation patterns + +--- + +### 📂 FILE SYSTEM RULES + +* All Kubernetes manifests and generated files must **use the /tmp directory exclusively**. +* If **Kubernetes configurations already exist in /tmp**, continue working within them instead of creating new ones. +* You must **never overwrite** existing configurations unless explicitly asked. + +--- + +### ⚙️ DEVELOPMENT WORKFLOW + +**Always use Context7 tools before creating K8s resources:** + +**Always list the files in a directory before creating new manifests.** + +**When applying K8s configurations, always wait 10 seconds after operation.** + +1. Clarify requirements and deployment architecture +2. Lookup Kubernetes and related technologies using Context7 tools +3. Retrieve current documentation, patterns, and best practices +4. Create or enhance configurations under /tmp, maintaining clean structure +5. Follow K8s conventions, security policies, and resource management +6. Include proper monitoring, logging, and health check configurations +7. Prioritize scalability, reliability, and operational excellence + +--- + +### ☸️ KUBERNETES RESOURCE RULES + +* **Use the latest Kubernetes API versions and best practices** +* **Follow security-first approach with RBAC, network policies, and pod security** +* **Structure should include:** + * Namespace definitions + * Deployment/StatefulSet manifests + * Service and Ingress configurations + * ConfigMaps and Secrets + * RBAC policies (ServiceAccount, Role, RoleBinding) + * NetworkPolicies for security + * HorizontalPodAutoscaler for scaling + * PodDisruptionBudget for availability + +**Supported Kubernetes Resources:** +* **Workloads:** Deployments, StatefulSets, DaemonSets, Jobs, CronJobs +* **Services:** ClusterIP, NodePort, LoadBalancer, ExternalName +* **Configuration:** ConfigMaps, Secrets, PersistentVolumes +* **Security:** RBAC, NetworkPolicies, PodSecurityPolicies +* **Scaling:** HPA, VPA, Cluster Autoscaler +* **Networking:** Ingress, Service Mesh (Istio/Linkerd) + +If Kubernetes configurations exist: +* Validate API versions and resource definitions +* Extend or modify as needed based on requirements +* Optimize for performance, security, and cost + +--- + +### 🧪 KUBERNETES ECOSYSTEM (verify latest versions with Context7) + +**Core:** kubectl, kubelet, kube-apiserver, etcd, kube-controller-manager +**Container Runtime:** containerd, Docker, CRI-O +**Networking:** Calico, Flannel, Weave, Cilium +**Service Mesh:** Istio, Linkerd, Consul Connect +**Monitoring:** Prometheus, Grafana, Jaeger, Kiali +**CI/CD:** ArgoCD, Flux, Tekton, Jenkins X +**Package Management:** Helm, Kustomize, Operator Framework +**Security:** Falco, Open Policy Agent (OPA), Twistlock +**Storage:** Longhorn, Rook, OpenEBS, Portworx + +--- + +### 🚀 COMMON KUBERNETES PATTERNS TO LEVERAGE + +* **Microservices Architecture** with proper service decomposition +* **GitOps Deployment** with declarative configurations +* **Blue-Green Deployments** for zero-downtime updates +* **Canary Releases** for safe rollouts +* **Resource Quotas** and limits for multi-tenancy +* **Health Checks** (liveness, readiness, startup probes) +* **Secrets Management** with external secret operators +* **Observability** with distributed tracing and metrics + +--- + +### 🛡️ SECURITY BEST PRACTICES + +* **Principle of Least Privilege** with RBAC +* **Network Segmentation** with NetworkPolicies +* **Pod Security Standards** enforcement +* **Image Security** scanning and admission controllers +* **Secret Rotation** and external secret management +* **Audit Logging** for compliance and monitoring +* **Resource Isolation** with namespaces and quotas + +--- + +### 📊 OPERATIONAL EXCELLENCE + +* **Infrastructure as Code** with Terraform/Pulumi +* **Automated Scaling** based on metrics +* **Disaster Recovery** planning and testing +* **Cost Optimization** with resource right-sizing +* **Performance Monitoring** and alerting +* **Capacity Planning** for growth +* **Multi-cluster Management** for resilience + +--- + +### ✅ SUMMARY + +* Always work in /tmp +* If K8s configurations exist, enhance them — don't recreate +* Use Context7 tools for everything: K8s decisions, patterns, and examples +* Follow security-first, cloud-native principles +* Adhere to modern best practices in cluster operations, security, and reliability +`; + } + + async initialize(): Promise { + console.log( + `☸️ Kubernetes Operations Agent initialized using ${this.config.model} on ${this.config.provider}\n` + ); + + let attempt = 0; + while (attempt < this.config.maxRetries) { + try { + // Health check + const isHealthy = await this.config.client.healthCheck(); + if (!isHealthy) { + console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); + process.exit(1); + } + + const tools = await this.config.client.listTools(); + const realContext7Tools = tools.data.filter((tool) => + ['c41_resolve-library-id', 'c41_get-library-docs'].includes(tool.name) + ); + const mockContext7Tools = tools.data.filter((tool) => + [ + 'search_libraries', + 'get_library_details', + 'get_documentation', + ].includes(tool.name) + ); + + const context7Tools = [...realContext7Tools, ...mockContext7Tools]; + + if (context7Tools.length === 0) { + console.error( + '⚠️ No Context7 MCP tools available. Make sure a Context7 MCP server is running.' + ); + console.error( + ' For real Context7: npx -y @upstash/context7-mcp@latest' + ); + console.error( + ' For local mock: docker-compose up --build (already included in this project)' + ); + process.exit(1); + } + + const usingRealContext7 = realContext7Tools.length > 0; + const toolType = usingRealContext7 ? 'real Context7' : 'mock Context7'; + + console.info( + `📋 Found ${context7Tools.length} ${toolType} tools available:` + ); + context7Tools.forEach((tool, index) => { + console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); + }); + + if (!usingRealContext7) { + console.info('💡 Using local mock Context7 server for demonstration'); + console.info( + ' To use real Context7 with latest docs, install: npx -y @upstash/context7-mcp@latest' + ); + } + console.info(''); + + this.showWelcomeMessage(); + await this.startInteractiveSession(); + break; + } catch (error) { + attempt++; + console.error( + `❌ Initialization Error (attempt ${attempt}/${this.config.maxRetries}):`, + (error as Error).message + ); + + if (attempt < this.config.maxRetries) { + console.log( + `⏳ Retrying in ${this.config.retryDelayMs / 1000} seconds...` + ); + await this.delay(this.config.retryDelayMs); + } else { + console.error( + `❌ Failed to initialize after ${this.config.maxRetries} attempts` + ); + console.log( + '\n💡 Make sure the Context7 MCP server is running on port 3002' + ); + console.log(' and the Inference Gateway is running on port 8080'); + process.exit(1); + } + } + } + } + + private showWelcomeMessage(): void { + console.log('☸️ Welcome to Kubernetes Operations Agent!'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log( + '\n💡 I specialize in Kubernetes cluster operations and container orchestration.' + ); + console.log( + " Just describe what you want to deploy or manage, and I'll use Context7" + ); + console.log( + ' to get up-to-date K8s documentation and create production-ready solutions.' + ); + console.log('\n📝 Example requests:'); + console.log( + ' • "Deploy a scalable web application with load balancing and auto-scaling"' + ); + console.log( + ' • "Create a microservices architecture with service mesh and monitoring"' + ); + console.log( + ' • "Set up a CI/CD pipeline with GitOps and automated deployments"' + ); + console.log( + ' • "Configure RBAC and network policies for multi-tenant cluster"' + ); + console.log( + ' • "Deploy a database cluster with persistent storage and backups"' + ); + console.log('\n⚡ Commands:'); + console.log(' • Type your request to start building'); + console.log(' • Use "clear" to reset conversation history'); + console.log(' • Use "exit" or "quit" to end the session'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + } + + private async startInteractiveSession(): Promise { + while (true) { + const userInput = await this.getUserInput( + '☸️ What would you like to deploy or manage in Kubernetes? ' + ); + + if (this.handleSpecialCommands(userInput)) { + continue; + } + + if (userInput.trim()) { + await this.processUserRequestWithRetry(userInput); + } + } + } + + private async getUserInput(prompt: string): Promise { + return new Promise((resolve) => { + this.rl.question(prompt, (answer) => { + resolve(answer); + }); + }); + } + + private handleSpecialCommands(input: string): boolean { + const command = input.trim().toLowerCase(); + + switch (command) { + case 'exit': + case 'quit': + console.log('\n👋 Thank you for using Kubernetes Agent! Goodbye!'); + this.rl.close(); + process.exit(0); + return true; + + case 'clear': + this.config.conversationHistory = [ + { + role: MessageRole.system, + content: this.getSystemPrompt(), + }, + ]; + console.log('\n🧹 Conversation history cleared. Starting fresh!\n'); + return true; + + case 'help': + this.showWelcomeMessage(); + return true; + + default: + return false; + } + } + + private async processUserRequestWithRetry(userInput: string): Promise { + let attempt = 0; + + while (attempt < this.config.maxRetries) { + try { + await this.processUserRequest(userInput); + break; + } catch (error) { + attempt++; + console.error( + `❌ Request failed (attempt ${attempt}/${this.config.maxRetries}):`, + (error as Error).message + ); + + if (attempt < this.config.maxRetries) { + console.log( + `⏳ Retrying in ${this.config.retryDelayMs / 1000} seconds...` + ); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + await this.delay(this.config.retryDelayMs); + } else { + console.error( + `❌ Failed to process request after ${this.config.maxRetries} attempts` + ); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + } + } + } + } + + private async processUserRequest(userInput: string): Promise { + console.log(`\n🔍 Processing Kubernetes request: "${userInput}"`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + this.config.conversationHistory.push({ + role: MessageRole.user, + content: userInput, + }); + + let assistantResponse = ''; + let shouldWaitForOperation = false; + + await this.config.client.streamChatCompletion( + { + model: `${this.config.provider}/${this.config.model}`, + messages: this.config.conversationHistory, + max_tokens: 2000, + }, + { + onOpen: () => { + console.log( + '🔗 Starting Kubernetes operations session with Context7...\n' + ); + }, + onReasoning: (reasoning) => { + console.log(`\n🤔 Agent Reasoning: ${reasoning}`); + }, + onContent: (content) => { + process.stdout.write(content); + assistantResponse += content; + }, + onMCPTool: (toolCall) => { + console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); + try { + const args = JSON.parse(toolCall.function.arguments); + console.log(`📝 Arguments:`, JSON.stringify(args, null, 2)); + } catch { + console.log(`📝 Raw Arguments: ${toolCall.function.arguments}`); + } + console.log(`🔍 Tool ID: ${toolCall.id}\n`); + + if ( + toolCall.function.name.toLowerCase().includes('kubernetes') || + toolCall.function.name.toLowerCase().includes('k8s') || + toolCall.function.name.toLowerCase().includes('kubectl') || + toolCall.function.name.toLowerCase().includes('deploy') || + toolCall.function.name.toLowerCase().includes('create') + ) { + console.log( + '☸️ Kubernetes operation detected - will wait 10 seconds after completion' + ); + shouldWaitForOperation = true; + } + }, + onError: (error) => { + console.error(`\n❌ Stream Error: ${error.error}`); + throw new Error(`Stream error: ${error.error}`); + }, + onFinish: async () => { + console.log('\n\n✅ Kubernetes operations session completed!\n'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + if (shouldWaitForOperation) { + await this.waitForKubernetesOperation(); + } + + // Add assistant response to conversation history + if (assistantResponse.trim()) { + this.config.conversationHistory.push({ + role: MessageRole.assistant, + content: assistantResponse, + }); + } + }, + } + ); + } + + async shutdown(): Promise { + this.rl.close(); + } +} + +async function runKubernetesAgent(): Promise { + const agent = new KubernetesAgent(); + + process.on('SIGINT', async () => { + console.log('\n\n👋 Shutting down Kubernetes Agent...'); + await agent.shutdown(); + process.exit(0); + }); + + process.on('SIGTERM', async () => { + console.log('\n\n👋 Shutting down Kubernetes Agent...'); + await agent.shutdown(); + process.exit(0); + }); + + await agent.initialize(); +} + +if ( + require.main === module || + process.argv[1].endsWith('kubernetes-agent.ts') +) { + runKubernetesAgent().catch(console.error); +} + +export { KubernetesAgent, runKubernetesAgent }; diff --git a/examples/mcp/example-list-tools.ts b/examples/mcp/example-list-tools.ts deleted file mode 100644 index 2f7ffce..0000000 --- a/examples/mcp/example-list-tools.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as dotenv from 'dotenv'; -import { InferenceGatewayClient } from '../../src/index.js'; - -dotenv.config(); - -(async () => { - const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', - }); - - try { - const tools = await client.listTools(); - console.info(`📋 Found ${tools.data.length} MCP tools:\n`); - - tools.data.forEach((tool, index) => { - console.info(`${index + 1}. ${tool.name}`); - console.info(` Description: ${tool.description}`); - if (tool.input_schema) { - console.info( - ` Input Schema:`, - JSON.stringify(tool.input_schema, null, 2) - ); - } - console.info(''); - }); - } catch (error) { - console.error('❌ Error:', error); - } -})(); diff --git a/examples/mcp/example-mcp-tools.ts b/examples/mcp/example-mcp-tools.ts deleted file mode 100644 index 23611a7..0000000 --- a/examples/mcp/example-mcp-tools.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { - InferenceGatewayClient, - MessageRole, - Provider, -} from '../../src/index.js'; - -/** - * Demonstration of MCP filesystem operations with proper directory access - * This example shows how to work with the /shared and /tmp directories - * and demonstrates the onMCPTool callback for tracking tool usage - */ -(async () => { - const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', - }); - - const provider = (process.env.PROVIDER as Provider) || Provider.groq; - const model = process.env.LLM || 'llama-3.3-70b-versatile'; - - console.info(`Using model: ${model}`); - console.info(`Using provider: ${provider}\n`); - - console.info('=== MCP Tool Usage Demo ===\n'); - - try { - // Check gateway health - console.info('🔍 Checking gateway health...'); - const isHealthy = await client.healthCheck(); - console.info( - `Gateway health: ${isHealthy ? '✅ Healthy' : '❌ Unhealthy'}\n` - ); - - if (!isHealthy) { - console.info( - 'Please ensure the Inference Gateway is running with Docker Compose.' - ); - process.exit(1); - } - - // List available models for the provider - console.info('🔍 Checking available models...'); - try { - const models = await client.listModels(provider); - console.info(`Found ${models.data.length} models for ${provider}:`); - models.data.forEach((modelInfo, index) => { - console.info(`${index + 1}. ${modelInfo.id}`); - }); - - // Check if the requested model is available - const isModelAvailable = models.data.some( - (m) => m.id === `${provider}/${model}` - ); - if (!isModelAvailable) { - console.info( - `⚠️ Model '${model}' not found for provider '${provider}'` - ); - console.info( - 'Consider using one of the available models listed above.' - ); - console.info( - 'You can set the LLM environment variable to use a different model.\n' - ); - } - } catch (modelError) { - console.info('⚠️ Could not retrieve model list:', modelError); - } - console.info(''); - - // List available MCP tools - console.info('📋 Listing available MCP tools...'); - const tools = await client.listTools(); - console.info(`Found ${tools.data.length} MCP tools:\n`); - - const fileTools = tools.data.filter((tool) => - ['read_file', 'write_file', 'list_directory'].includes(tool.name) - ); - - if (fileTools.length === 0) { - console.info('⚠️ No filesystem MCP tools available.'); - return; - } - - console.info('📁 Available filesystem tools:'); - fileTools.forEach((tool, index) => { - console.info(`${index + 1}. ${tool.name} - ${tool.description}`); - }); - console.info(''); - - // Track MCP tool calls for demonstration - const toolCallTracker = { - totalCalls: 0, - toolsUsed: new Set(), - filesAccessed: new Set(), - }; - - // Example: Analyze highest revenue from sales data - console.info('=== Highest Revenue Analysis with MCP Tool Tracking ===\n'); - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.system, - content: `You are a data analyst with filesystem access. You have access to tools that can read files from /tmp directory and write files to /tmp directory. When analyzing data, be thorough and provide specific insights.`, - }, - { - role: MessageRole.user, - content: - 'Please read the sample_sales_data.csv file from the /tmp directory, analyze it to find the highest revenue transactions, and create a detailed summary report. Save the summary to /tmp/revenue_analysis.txt.', - }, - ], - max_tokens: 1500, - }, - { - onOpen: () => { - console.info('🚀 Starting revenue analysis...'); - console.info('📊 MCP Tool usage will be tracked below:\n'); - }, - onContent: (content) => process.stdout.write(content), - onMCPTool: (toolCall) => { - toolCallTracker.totalCalls++; - toolCallTracker.toolsUsed.add(toolCall.function.name); - - console.info( - `\n🔧 [TOOL CALL #${toolCallTracker.totalCalls}] ${toolCall.function.name}` - ); - - const args = JSON.parse(toolCall.function.arguments); - - switch (toolCall.function.name) { - case 'read_file': - console.info(` 📄 Reading file: ${args.path}`); - toolCallTracker.filesAccessed.add(args.path); - break; - case 'write_file': - console.info(` 💾 Writing file: ${args.path}`); - console.info( - ` 📝 Content length: ${ - args.content ? args.content.length : 0 - } characters` - ); - toolCallTracker.filesAccessed.add(args.path); - break; - case 'list_directory': - console.info(` 📂 Listing directory: ${args.path}`); - break; - default: - console.info( - ` ⚙️ Arguments: ${JSON.stringify(args, null, 2)}` - ); - } - console.info(` 🆔 Tool ID: ${toolCall.id}`); - }, - onFinish: () => { - console.info('\n\n✅ Revenue analysis completed!\n'); - - // Display tool usage summary - console.info('📈 MCP Tool Usage Summary:'); - console.info(` Total tool calls: ${toolCallTracker.totalCalls}`); - console.info( - ` Tools used: ${Array.from(toolCallTracker.toolsUsed).join(', ')}` - ); - console.info( - ` Files accessed: ${Array.from( - toolCallTracker.filesAccessed - ).join(', ')}` - ); - console.info(''); - }, - onError: (error) => console.error('❌ Error:', error), - }, - provider - ); - - console.info('🎉 MCP Tool Usage Demo completed successfully!'); - console.info('\n💡 Key takeaways:'); - console.info( - '- The onMCPTool callback provides detailed tracking of tool usage' - ); - console.info( - '- Track total tool calls, tool types used, and files accessed' - ); - console.info( - '- Each tool call includes function name, arguments, and unique ID' - ); - console.info( - '- Perfect for debugging, monitoring, and understanding AI tool usage patterns' - ); - console.info( - '- LLM can read CSV data and perform complex analysis with file operations\n' - ); - } catch (error) { - if ( - error instanceof Error && - error.message.includes('MCP tools endpoint is not exposed') - ) { - console.error( - '❌ MCP tools are not exposed. Please ensure the Inference Gateway is started with MCP_EXPOSE=true' - ); - console.info('\n💡 To fix this, restart the gateway with:'); - console.info(' docker-compose up --build'); - } else { - console.error('❌ Error:', error); - } - } -})(); diff --git a/examples/mcp/example-agent.ts b/examples/mcp/example-nextjs-agent.ts similarity index 97% rename from examples/mcp/example-agent.ts rename to examples/mcp/example-nextjs-agent.ts index ef1878a..8a04bba 100644 --- a/examples/mcp/example-agent.ts +++ b/examples/mcp/example-nextjs-agent.ts @@ -14,7 +14,6 @@ import { Provider, } from '../../src/index.js'; -// Load environment variables from the mcp directory dotenv.config({ path: path.join(__dirname, '.env') }); declare const require: any; @@ -42,7 +41,7 @@ class Context7Agent { model: process.env.LLM || 'llama-3.3-70b-versatile', conversationHistory: [], maxRetries: 3, - retryDelayMs: 60000, // 1 minute + retryDelayMs: 60000, }; this.rl = readline.createInterface({ @@ -50,7 +49,6 @@ class Context7Agent { output: process.stdout, }); - // Initialize system prompt with comprehensive development instructions this.config.conversationHistory.push({ role: MessageRole.system, content: this.getSystemPrompt(), @@ -233,7 +231,7 @@ If a Next.js project exists: this.showWelcomeMessage(); await this.startInteractiveSession(); - break; // Success, exit retry loop + break; } catch (error) { attempt++; console.error( @@ -352,7 +350,7 @@ If a Next.js project exists: while (attempt < this.config.maxRetries) { try { await this.processUserRequest(userInput); - break; // Success, exit retry loop + break; } catch (error) { attempt++; console.error( @@ -380,7 +378,6 @@ If a Next.js project exists: console.log(`\n🔍 Processing request: "${userInput}"`); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - // Add user message to conversation history this.config.conversationHistory.push({ role: MessageRole.user, content: userInput, @@ -460,7 +457,6 @@ If a Next.js project exists: async function runContext7Agent(): Promise { const agent = new Context7Agent(); - // Handle graceful shutdown process.on('SIGINT', async () => { console.log('\n\n👋 Shutting down Context7 Agent...'); await agent.shutdown(); @@ -476,7 +472,6 @@ async function runContext7Agent(): Promise { await agent.initialize(); } -// Run the agent if (require.main === module || process.argv[1].endsWith('context7-agent.ts')) { runContext7Agent().catch(console.error); } diff --git a/examples/mcp/example-npm.ts b/examples/mcp/example-npm.ts deleted file mode 100644 index 0d9b916..0000000 --- a/examples/mcp/example-npm.ts +++ /dev/null @@ -1,194 +0,0 @@ -/** - * NPM MCP Server Usage Example - * - * This example demonstrates how to use the NPM MCP server - * to manage Node.js projects through the LLM with proper tool usage. - */ - -import * as dotenv from 'dotenv'; -import { - InferenceGatewayClient, - MessageRole, - Provider, -} from '../../src/index.js'; - -dotenv.config(); - -// For ES modules compatibility -declare const require: any; -declare const module: any; - -async function demonstrateNpmMcp() { - const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', - timeout: 120000, // 2 minute timeout for npm operations - }); - - const provider = (process.env.PROVIDER as Provider) || Provider.groq; - const model = process.env.LLM || 'llama-3.3-70b-versatile'; - - console.log(`🚀 NPM MCP Server Demo using ${model} on ${provider}\n`); - - try { - // Health check - const isHealthy = await client.healthCheck(); - if (!isHealthy) { - console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); - process.exit(1); - } - - // Check if NPM tools are available - const tools = await client.listTools(); - const npmTools = tools.data.filter((tool) => - ['npm_run', 'npm_init_project', 'npm_install_package'].includes(tool.name) - ); - - if (npmTools.length === 0) { - console.error( - '⚠️ NPM MCP tools not available. Make sure the NPM server is running on port 3003.' - ); - return; - } - - console.info(`📋 Found ${npmTools.length} NPM tools available:`); - npmTools.forEach((tool, index) => { - console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); - }); - console.info(''); - - // Demonstrate NPM usage through LLM conversation - console.info('=== NPM Demo: Project Setup and Management ===\n'); - - const userPrompt = `I want to create a new Node.js project with Express.js. Please help me by: - -1. Initialize a new npm project called "express-demo" in the /tmp directory -2. Install express, cors, and dotenv packages -3. List the installed packages to verify -4. Run npm audit to check for vulnerabilities -5. Show me the package.json that was created - -Please use the npm MCP tools to perform these operations safely.`; - - console.info(`User: ${userPrompt}`); - - await client.streamChatCompletion( - { - model: `${provider + '/' + model}`, - messages: [ - { - role: MessageRole.system, - content: `You are a helpful Node.js development assistant with access to NPM MCP tools for package management. - -IMPORTANT: You MUST use npm MCP tools to complete npm-related tasks. Available tools: - -1. Use npm_init_project to initialize new npm projects -2. Use npm_install_package to install packages -3. Use npm_run to execute npm commands like list, audit, etc. - -When helping with npm tasks: -- Always use the appropriate MCP tool for each operation -- Provide clear explanations of what each command does -- Show the results and explain any issues found -- Use /tmp as the working directory unless specified otherwise - -Be thorough and always use npm MCP tools to perform actual npm operations.`, - }, - { - role: MessageRole.user, - content: userPrompt, - }, - ], - max_tokens: 1500, - }, - { - onOpen: () => { - console.info('🔗 Connection opened, starting NPM session...\n'); - }, - onReasoning: (reasoning) => { - console.info(`\n🤔 NPM Reasoning: ${reasoning}`); - }, - onContent: (content) => { - process.stdout.write(content); - }, - onMCPTool: (toolCall) => { - console.info(`\n🛠️ NPM Tool: ${toolCall.function.name}`); - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`📝 Arguments:`, JSON.stringify(args, null, 2)); - } catch { - console.info(`📝 Raw Arguments: ${toolCall.function.arguments}`); - } - console.info(`🔍 Tool ID: ${toolCall.id}\n`); - }, - onError: (error) => { - console.error(`\n❌ Stream Error: ${error.error}`); - }, - onFinish: () => { - console.info('\n\n✅ NPM project setup completed!\n'); - }, - } - ); - - // Second example: Script management - console.info('\n=== NPM Demo: Script Management ===\n'); - - await client.streamChatCompletion( - { - model: `${provider + '/' + model}`, - messages: [ - { - role: MessageRole.system, - content: `You are a Node.js expert with access to NPM MCP tools. Always use npm MCP tools to manage packages and run commands.`, - }, - { - role: MessageRole.user, - content: `I have a project in /tmp/express-demo. Please help me: - -1. Check what scripts are available in the package.json -2. Install nodemon as a dev dependency -3. Run npm outdated to see if there are any package updates available -4. Show me how to check the project's dependency tree - -Use the npm MCP tools to perform these operations.`, - }, - ], - max_tokens: 1000, - }, - { - onOpen: () => { - console.info('🔗 Starting NPM script management session...\n'); - }, - onContent: (content) => { - process.stdout.write(content); - }, - onMCPTool: (toolCall) => { - console.info(`\n📦 NPM Tool: ${toolCall.function.name}`); - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`📋 Command:`, JSON.stringify(args, null, 2)); - } catch { - console.info(`📋 Raw Command: ${toolCall.function.arguments}`); - } - console.info(`🆔 Request ID: ${toolCall.id}\n`); - }, - onError: (error) => { - console.error(`\n❌ NPM Error: ${error.error}`); - }, - onFinish: () => { - console.info('\n\n✅ NPM script management session completed!\n'); - }, - } - ); - } catch (error) { - console.error('❌ Error:', (error as Error).message); - console.log('\n💡 Make sure the NPM MCP server is running on port 3003'); - console.log(' and the Inference Gateway is running on port 8080'); - } -} - -// Run the demo -if (require.main === module || process.argv[1].endsWith('example-npm.ts')) { - demonstrateNpmMcp().catch(console.error); -} - -export { demonstrateNpmMcp }; diff --git a/examples/mcp/example-tool-demo.ts b/examples/mcp/example-tool-demo.ts deleted file mode 100644 index 6c214d7..0000000 --- a/examples/mcp/example-tool-demo.ts +++ /dev/null @@ -1,221 +0,0 @@ -import * as dotenv from 'dotenv'; -import { - InferenceGatewayClient, - MessageRole, - Provider, -} from '../../src/index.js'; - -dotenv.config(); - -(async () => { - const client = new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', - }); - - const provider = (process.env.PROVIDER as Provider) || Provider.openai; - const model = process.env.LLM || 'gpt-4o'; - - console.info(`🔍 MCP Tool Demonstration - ${model} on ${provider}\n`); - console.info('='.repeat(60)); - console.info('🎯 PURPOSE: Demonstrate onMCPTool handler and identify issues'); - console.info('='.repeat(60)); - - try { - // Health check - const isHealthy = await client.healthCheck(); - if (!isHealthy) { - console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); - process.exit(1); - } - - // List available tools - const tools = await client.listTools(); - console.info(`\n📋 Available MCP Tools: ${tools.data.length}`); - tools.data.forEach((tool, index) => { - console.info( - ` ${index + 1}. ${tool.name} - ${tool.description.substring(0, 80)}...` - ); - }); - - console.info('\n' + '='.repeat(60)); - console.info('🧪 TEST 1: File Operations (Expected to work)'); - console.info('='.repeat(60)); - - let toolCallCount = 0; - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.user, - content: - 'Please create a simple test file at /tmp/test-demo.txt with the content "Hello MCP Demo!" and then read it back.', - }, - ], - max_tokens: 500, - }, - { - onContent: (content) => { - process.stdout.write(content); - }, - onMCPTool: (toolCall) => { - toolCallCount++; - console.info( - `\n📋 [${toolCallCount}] MCP Tool: ${toolCall.function.name}` - ); - console.info(`🆔 ID: ${toolCall.id}`); - console.info(`📝 Raw Args: ${toolCall.function.arguments}`); - - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`✅ Parsed Args:`, JSON.stringify(args, null, 2)); - - // Analyze arguments - if (args.path) { - console.info(`✅ ✓ Path parameter found: ${args.path}`); - } - if (args.content) { - console.info( - `✅ ✓ Content parameter found: ${args.content.substring(0, 50)}...` - ); - } - if (args.mcpServer) { - console.info(`✅ ✓ MCP Server specified: ${args.mcpServer}`); - } - } catch (e) { - console.info(`❌ Parse Error: ${e.message}`); - } - console.info(''); // spacing - }, - onFinish: () => { - console.info( - `\n✅ Test 1 Complete - File operations used ${toolCallCount} tools\n` - ); - runWebTest(); - }, - onError: (error) => { - console.error('\n❌ Test 1 Error:', error); - runWebTest(); - }, - }, - provider - ); - - async function runWebTest() { - console.info('='.repeat(60)); - console.info('🧪 TEST 2: Web Operations (Expected to have issues)'); - console.info('='.repeat(60)); - - toolCallCount = 0; - - await client.streamChatCompletion( - { - model, - messages: [ - { - role: MessageRole.user, - content: - 'Please fetch the content from https://example.com and show me what you find.', - }, - ], - max_tokens: 500, - }, - { - onContent: (content) => { - process.stdout.write(content); - }, - onMCPTool: (toolCall) => { - toolCallCount++; - console.info( - `\n📋 [${toolCallCount}] MCP Tool: ${toolCall.function.name}` - ); - console.info(`🆔 ID: ${toolCall.id}`); - console.info(`📝 Raw Args: ${toolCall.function.arguments}`); - - try { - const args = JSON.parse(toolCall.function.arguments); - console.info(`✅ Parsed Args:`, JSON.stringify(args, null, 2)); - - // Analyze web tool arguments - const urlFields = [ - 'url', - 'target_url', - 'webpage_url', - 'uri', - 'link', - ]; - const foundUrl = urlFields.find((field) => args[field]); - - if (foundUrl) { - console.info(`✅ ✓ URL parameter found: ${args[foundUrl]}`); - } else { - console.info(`❌ ✗ No URL parameter found`); - console.info( - `❌ ✗ Available fields: ${Object.keys(args).join(', ')}` - ); - console.info(`❌ ✗ Expected fields: ${urlFields.join(', ')}`); - } - - if (args.mcpServer) { - console.info(`✅ ✓ MCP Server specified: ${args.mcpServer}`); - } - } catch (e) { - console.info(`❌ Parse Error: ${e.message}`); - } - console.info(''); // spacing - }, - onFinish: () => { - console.info( - `\n✅ Test 2 Complete - Web operations used ${toolCallCount} tools\n` - ); - showSummary(); - }, - onError: (error) => { - console.error('\n❌ Test 2 Error:', error); - showSummary(); - }, - }, - provider - ); - } - - function showSummary() { - console.info('='.repeat(60)); - console.info('📊 SUMMARY AND FINDINGS'); - console.info('='.repeat(60)); - console.info(` -🎯 onMCPTool Handler Demonstration Complete! - -✅ WORKING FEATURES: - • onMCPTool callback properly invoked for all MCP tool calls - • File system operations work perfectly with complete arguments - • Tool ID tracking and argument parsing working - • MCP server routing works correctly - -❌ IDENTIFIED ISSUES: - • Web tools (fetch_url, search_web) receive incomplete arguments - • Missing URL parameter in web tool calls - • LLM receives incomplete tool schemas from inference gateway - • Tool schemas only show 'mcpServer' as required, missing actual parameters - -🔧 ROOT CAUSE: - The MCP web-search server's tool schemas are not being properly - exposed through the inference gateway. The gateway only shows - 'mcpServer' as a required parameter but misses tool-specific - parameters like 'url' for fetch_url. - -💡 RECOMMENDATIONS: - 1. Check inference gateway's MCP schema aggregation - 2. Verify web-search server's tool registration - 3. Ensure tool schemas are complete in gateway responses - 4. Test direct MCP server communication to compare schemas - -The onMCPTool handler itself works perfectly - the issue is with -the tool schema exposure in the inference gateway architecture. - `); - } - } catch (error) { - console.error('❌ Error:', error); - } -})(); diff --git a/examples/mcp/example-vite-agent.ts b/examples/mcp/example-vite-agent.ts new file mode 100644 index 0000000..912c3d7 --- /dev/null +++ b/examples/mcp/example-vite-agent.ts @@ -0,0 +1,505 @@ +/** + * Interactive Vite Development Agent + * + * This agent specializes in creating modern Vite-based applications with up-to-date + * documentation and best practices using Context7 MCP tools. + */ + +import * as dotenv from 'dotenv'; +import * as path from 'path'; +import * as readline from 'readline'; +import { + InferenceGatewayClient, + MessageRole, + Provider, +} from '../../src/index.js'; + +dotenv.config({ path: path.join(__dirname, '.env') }); + +declare const require: any; +declare const module: any; + +interface AgentConfig { + client: InferenceGatewayClient; + provider: Provider; + model: string; + conversationHistory: Array<{ role: MessageRole; content: string }>; + maxRetries: number; + retryDelayMs: number; +} + +class ViteAgent { + private config: AgentConfig; + private rl: readline.Interface; + + constructor() { + this.config = { + client: new InferenceGatewayClient({ + baseURL: 'http://localhost:8080/v1', + }), + provider: (process.env.PROVIDER as Provider) || Provider.groq, + model: process.env.LLM || 'llama-3.3-70b-versatile', + conversationHistory: [], + maxRetries: 3, + retryDelayMs: 60000, + }; + + this.rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + this.config.conversationHistory.push({ + role: MessageRole.system, + content: this.getSystemPrompt(), + }); + } + + private async delay(ms: number): Promise { + return new Promise((resolve) => { + global.setTimeout(() => resolve(), ms); + }); + } + + private async waitForViteProjectCreation(): Promise { + console.log( + '⏳ Waiting 15 seconds for Vite project creation to complete...' + ); + await this.delay(15000); + console.log('✅ Vite project creation wait period completed.\n'); + } + + private getSystemPrompt(): string { + return ` +You are an expert Vite development assistant with access to Context7 MCP tools for library documentation and research. Today is **June 1, 2025**. + +--- + +### 🔧 CORE RESPONSIBILITIES + +You help users create **modern, lightning-fast Vite applications** by: + +1. Understanding user requirements and recommending the best Vite-based stack +2. Using **Context7 tools** to retrieve up-to-date Vite documentation and best practices +3. Building complete projects with proper Vite configuration and optimization +4. Following modern development conventions and Vite-specific patterns +5. Creating fast, responsive, and well-structured applications + +--- + +### 🧰 CONTEXT7 TOOLS + +You have access to either **Real** or **Mock** Context7 tools. + +**Real Context7 Tools (@upstash/context7-mcp):** + +* c41_resolve-library-id: Resolve technology names to Context7-compatible IDs +* c41_get-library-docs: Fetch full documentation, usage examples, and best practices + +**Mock Tools (for local/demo use):** + +* search_libraries: Search for libraries by name or functionality +* get_library_details: Fetch library metadata and features +* get_documentation: Fetch usage examples and implementation patterns + +--- + +### 📂 FILE SYSTEM RULES + +* All projects and generated files must **use the /tmp directory exclusively**. +* If a **Vite project already exists in /tmp**, continue working within it instead of creating a new one. +* You must **never overwrite** an existing project unless explicitly asked. + +--- + +### ⚙️ DEVELOPMENT WORKFLOW + +**Always use Context7 tools before coding:** + +**Always list the files in a directory before creating new files.** + +**When creating a Vite project, always wait 15 seconds after project creation.** + +1. Clarify requirements and tech stack +2. Lookup Vite and related technologies using Context7 tools +3. Retrieve current documentation and patterns +4. Scaffold or enhance projects under /tmp, maintaining clean structure +5. Follow Vite conventions and optimization patterns +6. Include proper build configuration, testing, and development scripts +7. Prioritize maintainability, performance, and developer experience + +--- + +### ⚡ VITE PROJECT RULES + +* **Use the latest Vite configuration patterns and best practices** +* **Optimize for development speed and build performance** +* **Structure should include:** + * vite.config.js/ts – main configuration file + * index.html – entry point + * src/main.js/ts – application entry + * src/App.vue/jsx/tsx – main component + * public/ - static assets + * src/components/, src/assets/, etc. as needed + +**Supported Frameworks with Vite:** +* React (with TypeScript/JavaScript) +* Vue 3 (with TypeScript/JavaScript) +* Svelte/SvelteKit +* Vanilla JavaScript/TypeScript +* Preact +* Lit +* Solid + +If a Vite project exists: +* Validate configuration and structure +* Extend or modify as needed based on the request +* Optimize build and development settings + +--- + +### 🧪 VITE ECOSYSTEM (verify latest versions with Context7) + +**Core:** Vite, Rollup, ES Modules, Hot Module Replacement (HMR) +**Frontend Frameworks:** React, Vue, Svelte, Preact, Lit, Solid +**Styling:** Tailwind CSS, PostCSS, CSS Modules, Sass/SCSS, Styled Components +**Testing:** Vitest, Playwright, Cypress, Jest +**Build Tools:** ESBuild, SWC, Rollup plugins +**Utilities:** TypeScript, ESLint, Prettier, Autoprefixer +**Package Managers:** npm, yarn, pnpm, bun + +--- + +### 🚀 COMMON VITE FEATURES TO LEVERAGE + +* **Fast Development Server** with HMR +* **Optimized Build** with code splitting +* **Plugin Ecosystem** for extensibility +* **TypeScript Support** out of the box +* **CSS Preprocessing** and PostCSS +* **Asset Optimization** and bundling +* **Environment Variables** management +* **Proxy Configuration** for API development + +--- + +### ✅ SUMMARY + +* Always work in /tmp +* If a Vite project exists, enhance it — don't recreate +* Use Context7 tools for everything: Vite decisions, patterns, and examples +* Leverage Vite's speed and modern tooling advantages +* Adhere to modern best practices in project setup, UI/UX, and code quality +`; + } + + async initialize(): Promise { + console.log( + `⚡ Vite Development Agent initialized using ${this.config.model} on ${this.config.provider}\n` + ); + + let attempt = 0; + while (attempt < this.config.maxRetries) { + try { + // Health check + const isHealthy = await this.config.client.healthCheck(); + if (!isHealthy) { + console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); + process.exit(1); + } + + const tools = await this.config.client.listTools(); + const realContext7Tools = tools.data.filter((tool) => + ['c41_resolve-library-id', 'c41_get-library-docs'].includes(tool.name) + ); + const mockContext7Tools = tools.data.filter((tool) => + [ + 'search_libraries', + 'get_library_details', + 'get_documentation', + ].includes(tool.name) + ); + + const context7Tools = [...realContext7Tools, ...mockContext7Tools]; + + if (context7Tools.length === 0) { + console.error( + '⚠️ No Context7 MCP tools available. Make sure a Context7 MCP server is running.' + ); + console.error( + ' For real Context7: npx -y @upstash/context7-mcp@latest' + ); + console.error( + ' For local mock: docker-compose up --build (already included in this project)' + ); + process.exit(1); + } + + const usingRealContext7 = realContext7Tools.length > 0; + const toolType = usingRealContext7 ? 'real Context7' : 'mock Context7'; + + console.info( + `📋 Found ${context7Tools.length} ${toolType} tools available:` + ); + context7Tools.forEach((tool, index) => { + console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); + }); + + if (!usingRealContext7) { + console.info('💡 Using local mock Context7 server for demonstration'); + console.info( + ' To use real Context7 with latest docs, install: npx -y @upstash/context7-mcp@latest' + ); + } + console.info(''); + + this.showWelcomeMessage(); + await this.startInteractiveSession(); + break; + } catch (error) { + attempt++; + console.error( + `❌ Initialization Error (attempt ${attempt}/${this.config.maxRetries}):`, + (error as Error).message + ); + + if (attempt < this.config.maxRetries) { + console.log( + `⏳ Retrying in ${this.config.retryDelayMs / 1000} seconds...` + ); + await this.delay(this.config.retryDelayMs); + } else { + console.error( + `❌ Failed to initialize after ${this.config.maxRetries} attempts` + ); + console.log( + '\n💡 Make sure the Context7 MCP server is running on port 3002' + ); + console.log(' and the Inference Gateway is running on port 8080'); + process.exit(1); + } + } + } + } + + private showWelcomeMessage(): void { + console.log('⚡ Welcome to Vite Interactive Development Agent!'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log( + '\n💡 I specialize in creating lightning-fast Vite applications with modern tooling.' + ); + console.log( + " Just describe what you want to build, and I'll use Context7 to get" + ); + console.log( + ' up-to-date Vite documentation and create an optimized solution for you.' + ); + console.log('\n📝 Example requests:'); + console.log( + ' • "Create a React + TypeScript app with Vite and Tailwind CSS"' + ); + console.log( + ' • "Build a Vue 3 dashboard with Vite, Vitest, and component library"' + ); + console.log( + ' • "Make a Svelte SPA with Vite and optimal build configuration"' + ); + console.log( + ' • "Create a vanilla TypeScript app with Vite and modern tooling"' + ); + console.log('\n⚡ Commands:'); + console.log(' • Type your request to start building'); + console.log(' • Use "clear" to reset conversation history'); + console.log(' • Use "exit" or "quit" to end the session'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + } + + private async startInteractiveSession(): Promise { + while (true) { + const userInput = await this.getUserInput( + '⚡ What Vite application would you like to build? ' + ); + + if (this.handleSpecialCommands(userInput)) { + continue; + } + + if (userInput.trim()) { + await this.processUserRequestWithRetry(userInput); + } + } + } + + private async getUserInput(prompt: string): Promise { + return new Promise((resolve) => { + this.rl.question(prompt, (answer) => { + resolve(answer); + }); + }); + } + + private handleSpecialCommands(input: string): boolean { + const command = input.trim().toLowerCase(); + + switch (command) { + case 'exit': + case 'quit': + console.log('\n👋 Thank you for using Vite Agent! Goodbye!'); + this.rl.close(); + process.exit(0); + return true; + + case 'clear': + this.config.conversationHistory = [ + { + role: MessageRole.system, + content: this.getSystemPrompt(), + }, + ]; + console.log('\n🧹 Conversation history cleared. Starting fresh!\n'); + return true; + + case 'help': + this.showWelcomeMessage(); + return true; + + default: + return false; + } + } + + private async processUserRequestWithRetry(userInput: string): Promise { + let attempt = 0; + + while (attempt < this.config.maxRetries) { + try { + await this.processUserRequest(userInput); + break; + } catch (error) { + attempt++; + console.error( + `❌ Request failed (attempt ${attempt}/${this.config.maxRetries}):`, + (error as Error).message + ); + + if (attempt < this.config.maxRetries) { + console.log( + `⏳ Retrying in ${this.config.retryDelayMs / 1000} seconds...` + ); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + await this.delay(this.config.retryDelayMs); + } else { + console.error( + `❌ Failed to process request after ${this.config.maxRetries} attempts` + ); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + } + } + } + } + + private async processUserRequest(userInput: string): Promise { + console.log(`\n🔍 Processing Vite request: "${userInput}"`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + this.config.conversationHistory.push({ + role: MessageRole.user, + content: userInput, + }); + + let assistantResponse = ''; + let shouldWaitForProject = false; + + await this.config.client.streamChatCompletion( + { + model: `${this.config.provider}/${this.config.model}`, + messages: this.config.conversationHistory, + max_tokens: 2000, + }, + { + onOpen: () => { + console.log( + '🔗 Starting Vite development session with Context7...\n' + ); + }, + onReasoning: (reasoning) => { + console.log(`\n🤔 Agent Reasoning: ${reasoning}`); + }, + onContent: (content) => { + process.stdout.write(content); + assistantResponse += content; + }, + onMCPTool: (toolCall) => { + console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); + try { + const args = JSON.parse(toolCall.function.arguments); + console.log(`📝 Arguments:`, JSON.stringify(args, null, 2)); + } catch { + console.log(`📝 Raw Arguments: ${toolCall.function.arguments}`); + } + console.log(`🔍 Tool ID: ${toolCall.id}\n`); + + if ( + toolCall.function.name === 'create_vite_project' || + toolCall.function.name === 'create_new_workspace' || + toolCall.function.name.toLowerCase().includes('vite') || + toolCall.function.name.toLowerCase().includes('project') + ) { + console.log( + '⚡ Vite project creation detected - will wait 15 seconds after completion' + ); + shouldWaitForProject = true; + } + }, + onError: (error) => { + console.error(`\n❌ Stream Error: ${error.error}`); + throw new Error(`Stream error: ${error.error}`); + }, + onFinish: async () => { + console.log('\n\n✅ Vite development session completed!\n'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + if (shouldWaitForProject) { + await this.waitForViteProjectCreation(); + } + + // Add assistant response to conversation history + if (assistantResponse.trim()) { + this.config.conversationHistory.push({ + role: MessageRole.assistant, + content: assistantResponse, + }); + } + }, + } + ); + } + + async shutdown(): Promise { + this.rl.close(); + } +} + +async function runViteAgent(): Promise { + const agent = new ViteAgent(); + + process.on('SIGINT', async () => { + console.log('\n\n👋 Shutting down Vite Agent...'); + await agent.shutdown(); + process.exit(0); + }); + + process.on('SIGTERM', async () => { + console.log('\n\n👋 Shutting down Vite Agent...'); + await agent.shutdown(); + process.exit(0); + }); + + await agent.initialize(); +} + +if (require.main === module || process.argv[1].endsWith('vite-agent.ts')) { + runViteAgent().catch(console.error); +} + +export { runViteAgent, ViteAgent }; diff --git a/examples/mcp/mcp-servers/filesystem/index.js b/examples/mcp/mcp-servers/filesystem/index.js index 8f597e8..142f790 100644 --- a/examples/mcp/mcp-servers/filesystem/index.js +++ b/examples/mcp/mcp-servers/filesystem/index.js @@ -22,9 +22,9 @@ app.use(express.json()); const transports = {}; // Allowed directories (configurable via environment) -const allowedDirectories = ( - process.env.ALLOWED_DIRECTORIES || '/shared,/tmp' -).split(','); +const allowedDirectories = (process.env.ALLOWED_DIRECTORIES || '/tmp').split( + ',' +); console.info('Allowed directories:', allowedDirectories); @@ -48,7 +48,6 @@ function createMcpServer() { version: '1.0.0', }); - // Tool: Read file content mcpServer.tool( 'read_file', { @@ -99,7 +98,6 @@ function createMcpServer() { } ); - // Tool: Write file content mcpServer.tool( 'write_file', { @@ -116,7 +114,6 @@ function createMcpServer() { try { console.info(`Writing to file: ${filePath}`); - // Ensure directory exists const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); @@ -145,7 +142,6 @@ function createMcpServer() { } ); - // Tool: List directory contents mcpServer.tool( 'list_directory', { @@ -232,7 +228,6 @@ function createMcpServer() { } ); - // Tool: Create directory mcpServer.tool( 'create_directory', { @@ -273,7 +268,6 @@ function createMcpServer() { } ); - // Tool: Delete file mcpServer.tool( 'delete_file', { @@ -326,7 +320,6 @@ function createMcpServer() { } ); - // Tool: Get file info mcpServer.tool( 'file_info', { @@ -416,8 +409,6 @@ function setupSessionRoutes() { console.info(' Headers: %s', JSON.stringify(req.headers, null, 2)); console.info(' Body: %s', JSON.stringify(req.body, null, 2)); - // Fix missing Accept headers for compatibility with Go MCP clients - // The StreamableHTTPServerTransport requires both application/json and text/event-stream const accept = req.headers.accept || req.headers.Accept; if ( !accept || @@ -428,25 +419,20 @@ function setupSessionRoutes() { req.headers.accept = 'application/json, text/event-stream'; } - // Check for existing session ID const sessionId = req.headers['mcp-session-id']; let transport; if (sessionId && transports[sessionId]) { - // Reuse existing transport transport = transports[sessionId]; } else { - // Create new transport for new session transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (newSessionId) => { console.info(`MCP session initialized: ${newSessionId}`); - // Store the transport by session ID transports[newSessionId] = transport; }, }); - // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { console.info(`MCP session closed: ${transport.sessionId}`); @@ -454,12 +440,10 @@ function setupSessionRoutes() { } }; - // Create and connect MCP server const server = createMcpServer(); await server.connect(transport); } - // Handle the MCP request await transport.handleRequest(req, res, req.body); } catch (error) { console.error('Error handling MCP request:', error); @@ -475,67 +459,6 @@ function setupSessionRoutes() { } } }); - - // Handle GET requests for SSE (server-to-client notifications) - app.get('/mcp', async (req, res) => { - const sessionId = req.headers['mcp-session-id']; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); - return; - } - - const transport = transports[sessionId]; - await transport.handleRequest(req, res); - }); - - // Handle DELETE requests for session termination - app.delete('/mcp', async (req, res) => { - const sessionId = req.headers['mcp-session-id']; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); - return; - } - - const transport = transports[sessionId]; - await transport.handleRequest(req, res); - }); -} - -/** - * Initialize sample files - */ -async function initializeSampleFiles() { - try { - // Create sample files in allowed directories - for (const dir of allowedDirectories) { - try { - await fs.access(dir); - - const sampleFile = path.join(dir, 'mcp-filesystem-example.txt'); - const sampleContent = `Hello from MCP Filesystem Server! - -This is a sample file created by the MCP Filesystem Server. -Created at: ${new Date().toISOString()} - -You can use the following MCP tools to interact with this file: -- read_file: Read this file's content -- write_file: Modify this file -- file_info: Get detailed information about this file -- list_directory: List all files in this directory -- delete_file: Delete this file - -Available directories: ${allowedDirectories.join(', ')} -`; - - await fs.writeFile(sampleFile, sampleContent); - console.info(`Created sample file: ${sampleFile}`); - } catch (error) { - console.info(`Could not create sample file in ${dir}:`, error.message); - } - } - } catch (error) { - console.error('Error initializing sample files:', error); - } } /** @@ -563,7 +486,6 @@ async function startServer() { const port = process.env.PORT || 3000; const host = process.env.HOST || '0.0.0.0'; - // Set up session routes setupSessionRoutes(); app.listen(port, host, async () => { @@ -588,17 +510,12 @@ async function startServer() { console.info(' - move_file - Move/rename a file'); console.info('Allowed directories:', allowedDirectories); - // Initialize sample files - await initializeSampleFiles(); - console.info('MCP Filesystem server ready for connections'); }); } -// Graceful shutdown process.on('SIGTERM', () => { console.info('Received SIGTERM, shutting down gracefully'); - // Close all transports Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); }); @@ -607,14 +524,12 @@ process.on('SIGTERM', () => { process.on('SIGINT', () => { console.info('Received SIGINT, shutting down gracefully'); - // Close all transports Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); }); process.exit(0); }); -// Start the server startServer().catch((error) => { console.error('Failed to start server:', error); process.exit(1); diff --git a/examples/mcp/package.json b/examples/mcp/package.json index 6251637..dfdceb8 100644 --- a/examples/mcp/package.json +++ b/examples/mcp/package.json @@ -6,17 +6,9 @@ "private": true, "scripts": { "clean": "rm -rf shared/next-app shared/update-check", - "start": "tsx example-basic.ts", - "example:basic": "tsx example-basic.ts", - "example:advanced": "tsx example-advanced.ts", - "example:nextjs": "tsx example-nextjs.ts", - "example:handler-demo": "tsx example-handler-demo.ts", - "example:tool-demo": "tsx example-tool-demo.ts", - "example:debug-args": "tsx example-debug-args.ts", - "example:test-direct-mcp": "tsx example-test-direct-mcp.ts", - "example:list-tools": "tsx example-list-tools.ts", - "example:mcp-tools": "tsx example-mcp-tools.ts", - "example:agent": "tsx example-agent.ts", + "example:nextjs-agent": "tsx example-nextjs-agent.ts", + "example:vite-agent": "tsx example-vite-agent.ts", + "example:kubernetes-agent": "tsx example-kubernetes-agent.ts", "compose:up": "docker-compose up -d", "compose:down": "docker-compose down", "compose:logs": "docker-compose logs -f" diff --git a/examples/mcp/shared/sample_sales_data.csv b/examples/mcp/shared/sample_sales_data.csv deleted file mode 100644 index 628aa2d..0000000 --- a/examples/mcp/shared/sample_sales_data.csv +++ /dev/null @@ -1,16 +0,0 @@ -Date,Product,Quantity,Revenue,Region -2025-01-01,Laptop,10,15000,North -2025-01-02,Mouse,25,500,South -2025-01-03,Keyboard,15,750,East -2025-01-04,Monitor,8,2400,West -2025-01-05,Laptop,12,18000,North -2025-01-06,Mouse,30,600,South -2025-01-07,Keyboard,20,1000,East -2025-01-08,Monitor,6,1800,West -2025-01-09,Laptop,8,12000,Central -2025-01-10,Mouse,35,700,North -2025-01-11,Keyboard,25,1250,South -2025-01-12,Monitor,10,3000,East -2025-01-13,Laptop,15,22500,West -2025-01-14,Mouse,40,800,Central -2025-01-15,Keyboard,18,900,North From 3934ad7e7484ae2843f16ff991c0380daf6c211c Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 14:10:31 +0000 Subject: [PATCH 16/38] chore: Add Vite agent and Docker Compose configuration for MCP - Created a new package.json for the Vite agent with necessary dependencies and scripts. - Added a Docker Compose configuration file to manage multiple agents including Kubernetes, Vite, and Next.js. - Removed the outdated README for MCP servers as part of the restructuring. Signed-off-by: Eden Reich --- examples/mcp/README.md | 811 +---------------- examples/mcp/agents/kubernetes/Dockerfile | 15 + .../kubernetes/index.ts} | 8 +- .../{ => agents/kubernetes}/package-lock.json | 0 .../mcp/{ => agents/kubernetes}/package.json | 0 examples/mcp/agents/nextjs/Dockerfile | 10 + .../nextjs/index.ts} | 10 +- examples/mcp/agents/nextjs/package-lock.json | 853 ++++++++++++++++++ examples/mcp/agents/nextjs/package.json | 21 + examples/mcp/agents/vite/Dockerfile | 10 + .../vite/index.ts} | 8 +- examples/mcp/agents/vite/package-lock.json | 853 ++++++++++++++++++ examples/mcp/agents/vite/package.json | 27 + examples/mcp/docker-compose-agents.yml | 41 + examples/mcp/mcp-servers/README.md | 274 ------ 15 files changed, 1846 insertions(+), 1095 deletions(-) create mode 100644 examples/mcp/agents/kubernetes/Dockerfile rename examples/mcp/{example-kubernetes-agent.ts => agents/kubernetes/index.ts} (99%) rename examples/mcp/{ => agents/kubernetes}/package-lock.json (100%) rename examples/mcp/{ => agents/kubernetes}/package.json (100%) create mode 100644 examples/mcp/agents/nextjs/Dockerfile rename examples/mcp/{example-nextjs-agent.ts => agents/nextjs/index.ts} (99%) create mode 100644 examples/mcp/agents/nextjs/package-lock.json create mode 100644 examples/mcp/agents/nextjs/package.json create mode 100644 examples/mcp/agents/vite/Dockerfile rename examples/mcp/{example-vite-agent.ts => agents/vite/index.ts} (99%) create mode 100644 examples/mcp/agents/vite/package-lock.json create mode 100644 examples/mcp/agents/vite/package.json create mode 100644 examples/mcp/docker-compose-agents.yml delete mode 100644 examples/mcp/mcp-servers/README.md diff --git a/examples/mcp/README.md b/examples/mcp/README.md index 55d6eb2..28424a0 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -7,813 +7,8 @@ This directory contains comprehensive examples demonstrating how to use the Infe ### Run Specific Examples ```bash - # Run interactive specialized agents -npm run example:nextjs-agent # 🤖 Next.js development agent -npm run example:vite-agent # ⚡ Vite application agent -npm run example:kubernetes-agent # ☸️ Kubernetes operations agent -``` - -## Architecture - -This example uses Docker Compose to orchestrate: - -- **Inference Gateway** - Main API gateway with MCP support enabled -- **MCP Filesystem Server** - Provides file system operations (restricted to `/shared` and `/tmp`) -- **MCP Web Search Server** - Simulated web search and URL fetching -- **Optional Ollama** - Local model inference (when using `--profile with-ollama`) - -## Important: Filesystem Access - -The MCP filesystem server is configured with restricted access for security: - -- **`/shared`** - Read-Write directory for shared files between the host and container -- **`/tmp`** - Read-write directory for temporary files inside of the container - -The AI will only be able to access these directories. This prevents unauthorized access to system files. - -## Sample Data - -The `/shared` directory contains example files for testing. - -## 🤖 Interactive Development Agents - -This repository includes three specialized interactive agents that use Context7 MCP tools for up-to-date documentation and best practices: - -### ⚡ Vite Agent (`example-vite-agent.ts`) - -**Purpose:** Creates lightning-fast modern Vite applications with optimal configuration. - -**Specializes in:** - -- React, Vue, Svelte, and vanilla JavaScript/TypeScript projects -- Modern build tooling and optimization -- Fast development server with HMR -- Vitest testing setup -- TypeScript configuration -- CSS preprocessing (Tailwind, Sass, PostCSS) - -**Run with:** `npm run example:vite-agent` - -**Example requests:** - -- "Create a React + TypeScript app with Vite and Tailwind CSS" -- "Build a Vue 3 dashboard with Vite, Vitest, and component library" -- "Make a Svelte SPA with Vite and optimal build configuration" - -### ☸️ Kubernetes Agent (`example-kubernetes-agent.ts`) - -**Purpose:** Handles Kubernetes cluster operations and container orchestration. - -**Specializes in:** - -- Production-ready YAML manifests -- Deployment strategies (blue-green, canary) -- RBAC and security policies -- Service mesh configuration -- Monitoring and observability -- Scaling and resource management -- CI/CD pipeline integration - -**Run with:** `npm run example:kubernetes-agent` - -**Example requests:** - -- "Deploy a scalable web application with load balancing and auto-scaling" -- "Create a microservices architecture with service mesh and monitoring" -- "Set up RBAC and network policies for multi-tenant cluster" -- "Configure GitOps deployment pipeline with ArgoCD" - -### 🤖 Next.js Agent (`example-nextjs-agent.ts`) - -**Purpose:** Creates modern Next.js applications with App Router and latest features. - -**Specializes in:** - -- Next.js 13+ App Router architecture -- Server Components and streaming -- TypeScript and modern tooling -- Performance optimization -- SEO and accessibility -- Production deployment - -**Run with:** `npm run example:nextjs-agent` - -**Example requests:** - -- "Create a Next.js blog with TypeScript and Tailwind CSS" -- "Build a React dashboard with charts and data visualization" -- "Make a full-stack Next.js app with authentication and database" - -### 🧰 Context7 Integration - -All agents use Context7 MCP tools to: - -- **Fetch Latest Documentation** - Get current best practices and API references -- **Resolve Library Dependencies** - Find compatible package versions -- **Access Code Examples** - Retrieve real-world implementation patterns -- **Stay Current** - Use up-to-date information instead of static training data - -Each agent maintains conversation history and provides interactive help. Use `clear` to reset, `help` for guidance, or `exit` to quit. - -## Setup Instructions - -### Prerequisites - -- Docker and Docker Compose installed -- API key for at least one provider (OpenAI, Groq, Anthropic, etc.) - -### 1. Environment Configuration - -Copy the example environment file: - -```bash -cp .env.example .env -``` - -Configure your environment: - -```bash -# Required: Set your provider and model -PROVIDER=openai -LLM=gpt-4o - -# Required: Add your API keys (at least one) -OPENAI_API_KEY=your_key_here -GROQ_API_KEY=your_key_here -ANTHROPIC_API_KEY=your_key_here -DEEPSEEK_API_KEY=your_key_here - -# Optional: Inference Gateway configuration -EXPOSE_MCP=true -``` - -### 2. Start Infrastructure - -Start the MCP infrastructure: - -```bash -npm run compose:up -``` - -Wait for all services to be healthy: - -```bash -docker-compose ps -``` - -### 3. Install Dependencies - -```bash -npm install -``` - -### 4. Run Examples - -Choose any example to run: - -```bash -# Start with the basic example -npm run start - -# Or run specific examples -npm run run:advanced -npm run run:nextjs -npm run run:tool-demo -``` - -```bash -cp .env.example .env -``` - -### 1. Start the MCP Infrastructure - -Start all services using Docker Compose: - -```bash -npm run compose:up -``` - -This will start: - -- Inference Gateway on port 8080 -- MCP Filesystem server on port 3000 -- MCP Web Search server on port 3001 - -### 2. Install Dependencies - -```bash -npm install -``` - -### 3. Configure Provider and Model - -Set your preferred provider and model: - -```bash -export PROVIDER=groq -export LLM=meta-llama/llama-3.3-70b-versatile -``` - -Or for OpenAI: - -```bash -export PROVIDER=openai -export LLM=gpt-4o -``` - -### 4. Verify MCP Setup - -Test that MCP tools are working correctly: - -```bash -npx tsx test-mcp-tools.ts -``` - -### 5. Run Examples - -Run the main example: - -```bash -npm start -``` - -Or run the focused filesystem demo: - -```bash -npx tsx filesystem-demo.ts -``` - -## Available Examples - -- `npm run example:nextjs-agent` - Next.js application generator -- `npm run example:vite-agent` - Vite application generator -- `npm run example:kubernetes-agent` - Kubernetes deployment example - -## Available Commands - -- `npm run compose:up` - Start all services in background -- `npm run compose:down` - Stop all services -- `npm run compose:logs` - View logs from all services - -## Key Features Demonstrated - -### Enhanced Tool Handling - -The examples now showcase: - -1. **Separate MCP and Regular Tool Handlers**: - - - `onMCPTool` - Handles tools from MCP servers - - `onTool` - Handles client-provided tools - - Enhanced logging with tool IDs and formatted arguments - -2. **Comprehensive Stream Callbacks**: - - - `onOpen` - Connection established - - `onContent` - Streaming content - - `onMCPTool` - MCP tool calls with detailed logging - - `onUsageMetrics` - Token usage tracking - - `onFinish` - Stream completion - -3. **Enhanced Error Handling**: - - - Graceful handling of malformed JSON in tool arguments - - Multiple fallback field names for URL parsing (`url`, `target_url`, `webpage_url`) - - Detailed logging of parse errors and missing parameters - -4. **Tool Argument Debugging**: - - Raw argument display for troubleshooting - - Structured argument parsing with error recovery - - Issue identification for incomplete tool schemas - -### Known Issues and Workarounds - -The examples demonstrate both working features and current limitations: - -**✅ Working:** - -- File system operations (read_file, write_file, list_directory, etc.) -- Complete argument passing with path, content, and mcpServer parameters -- Robust error handling and logging - -**❌ Current Issues:** - -- Web tools (fetch_url, search_web) receive incomplete arguments -- Missing URL parameters due to incomplete schema exposure -- LLM receives partial tool definitions from inference gateway - -**🔧 Demonstrated Solutions:** - -- Enhanced argument parsing with multiple field fallbacks -- Graceful error handling when tools fail -- Comprehensive logging to identify root causes -- Tool schema inspection utilities - -See `MCP_IMPROVEMENT_SUMMARY.md` for detailed analysis and findings. - -- `onError` - Error handling - -3. **Better Tool Call Visualization**: - - Tool call counting - - Formatted argument display - - Tool execution tracking - - Performance metrics - -## 🆕 Next.js App Creator Example - -The `nextjs-example.ts` demonstrates a powerful real-world use case: - -### What it does: - -1. **Fetches Official Documentation** - Uses MCP web tools to get the latest Next.js docs -2. **Creates Complete App Structure** - Builds a production-ready Next.js application -3. **Follows Best Practices** - Uses the fetched documentation to ensure current patterns -4. **TypeScript Setup** - Includes proper TypeScript configuration -5. **Modern Features** - Implements App Router, Server Components, and latest Next.js features - -### Features Demonstrated: - -- **Documentation-Driven Development** - AI reads official docs before coding -- **Complex File Operations** - Creates entire application structures -- **Web Content + File Operations** - Combines multiple MCP tool types -- **Production-Ready Output** - Generates runnable Next.js applications - -### Run the Example: - -```bash -npm run nextjs -``` - -The AI will: - -- Fetch Next.js documentation from https://nextjs.org/docs -- Create a complete application in `/tmp/nextjs-app/` -- Include package.json, configs, pages, components, and README -- Follow the latest Next.js best practices and conventions - -## Example Prompts to Try - -Once the example is running, you can ask the AI: - -1. **File Operations Chain:** - - ``` - "Create a JSON config file at /tmp/config.json with sample data, read it back, and list the directory" - ``` - -2. **Multi-step Analysis:** - - ``` - "Read the sales data from /shared/sample_sales_data.csv, analyze trends, and create a summary report" - ``` - -3. **Web Research:** - - ``` - "Fetch content from https://httpbin.org/json and tell me what information it contains" - ``` - -4. **Documentation-Based Development:** - - ``` - "Create a React component library by first fetching React documentation, then building reusable components" - ``` - -5. **Complex File Tasks:** - ``` - "Create a todo list with 5 tasks, save it to /tmp/todo.txt, then read it back and add 2 more tasks" - ``` - -## Example Output - -The example will demonstrate: - -``` -=== MCP Tools Example === - -🔍 Checking gateway health... -Gateway health: ✅ Healthy - -📋 Listing available MCP tools... -Found 9 MCP tools: - -1. read_file - Description: Read content from a file - Server: http://mcp-filesystem:3000/mcp - Schema: { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "Path to the file to read" - } - }, - "required": ["file_path"] - } - -2. write_file - Description: Write content to a file - Server: http://mcp-filesystem:3000/mcp - Schema: { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "Path to the file to write" - }, - "content": { - "type": "string", - "description": "Content to write to the file" - } - }, - "required": ["file_path", "content"] - } - -3. fetch_url - Description: Fetch content from a URL - Server: http://mcp-web-search:3001/mcp - Schema: { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "The URL to fetch content from" - }, - "timeout": { - "type": "number", - "description": "Request timeout in milliseconds" - } - }, - "required": ["url"] - } - -4. search_web - Description: Search the web and return results - Server: http://mcp-web-search:3001/mcp - Schema: { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query" - }, - "limit": { - "type": "number", - "description": "Number of results to return" - } - }, - "required": ["query"] - } - -=== Example 1: File Operations with MCP === - -🚀 Starting file reading example... -I'll help you read the contents of /tmp/example.txt file. - -🔧 Tool called: read_file -📝 Arguments: {"file_path": "/tmp/example.txt"} - -The file contains: -Hello from MCP filesystem server! -This is a sample file for testing MCP file operations. -Created at: Mon May 27 10:30:00 UTC 2025 - -✅ File reading example completed -``` - -## MCP Servers Included - -### Filesystem Server - -- **Purpose**: File system operations (read, write, list directories) -- **Port**: 3000 -- **Tools**: `read_file`, `write_file`, `list_directory` -- **Allowed Directories**: `/shared`, `/tmp` - -### Web Search Server - -- **Purpose**: Web content fetching and search simulation -- **Port**: 3001 -- **Tools**: `fetch_url`, `search_web` -- **Features**: HTTP requests, basic content extraction - -### Context7 Server - -- **Purpose**: Library documentation and context resolution -- **Port**: 3002 -- **Tools**: `resolve_library_id`, `get_library_docs`, `search_libraries` -- **Features**: Library search, documentation retrieval, version management - -## Supported Providers - -All Inference Gateway providers work with MCP tools: - -- `openai` - GPT models with excellent tool calling -- `groq` - Fast inference with Llama and Mixtral models -- `anthropic` - Claude models with strong reasoning -- `ollama` - Local models (use `--profile with-ollama`) -- `cohere` - Command models -- `deepseek` - DeepSeek models -- `cloudflare` - Workers AI models - -## Using with Local Models (Ollama) - -To include Ollama for local model inference: - -```bash -docker-compose --profile with-ollama up -d -``` - -Then pull a model: - -```bash -docker exec -it mcp_ollama_1 ollama pull llama3.2 -``` - -Set environment variables: - -```bash -export PROVIDER=ollama -export LLM=llama3.2 -``` - -## Troubleshooting - -### MCP Tools Not Available - -If you see "No MCP tools available": - -1. Check if MCP servers are running: - - ```bash - docker-compose ps - ``` - -2. Verify MCP server logs: - - ```bash - npm run compose:logs - ``` - -3. Ensure the Inference Gateway can reach MCP servers: - ```bash - curl -X POST http://localhost:8080/mcp/tools/list -H "Content-Type: application/json" -d '{}' - ``` - -### Gateway Health Check Fails - -If the gateway appears unhealthy: - -1. Check gateway logs: - - ```bash - docker-compose logs inference-gateway - ``` - -2. Verify API keys are set in `.env` file -3. Test direct connection: - ```bash - curl http://localhost:8080/health - ``` - -### Tool Calls Not Working - -If tool calls fail during conversations: - -1. Verify the model supports tool calling (GPT-4, Claude, etc.) -2. Check MCP server connectivity through the gateway: - - ```bash - curl -X POST http://localhost:8080/mcp/tools/list \ - -H "Content-Type: application/json" \ - -d '{}' - ``` - -3. Enable debug logging by adding to docker-compose.yml: - ```yaml - environment: - - LOG_LEVEL=debug - ``` - -## Custom MCP Servers - -To add your own MCP server: - -1. Add service to `docker-compose.yml`: - - ```yaml - my-custom-mcp: - image: my-mcp-server:latest - ports: - - '3002:3002' - networks: - - inference-network - ``` - -2. Update Inference Gateway environment: - - ```yaml - MCP_SERVERS: 'http://mcp-filesystem:3000/mcp,http://mcp-web-search:3001/mcp,http://my-custom-mcp:3002/mcp' - ``` - -3. Restart services: - ```bash - npm run compose:down - npm run compose:up - ``` - -## Notes - -- MCP tools are called automatically by AI models when relevant to the conversation -- Tool schemas are defined by the MCP servers and exposed through the `/mcp/tools` endpoint -- Each MCP server can provide multiple tools with different capabilities -- The Inference Gateway acts as a bridge between AI models and MCP tools -- Tool execution is streamed in real-time during conversations -- File operations are sandboxed to allowed directories for security - -## Resources - -- [Model Context Protocol Specification](https://modelcontextprotocol.io/) -- [Inference Gateway Documentation](https://github.com/inference-gateway/inference-gateway) -- [Official MCP Servers](https://github.com/modelcontextprotocol/servers) - -## HTTP Examples - -This section provides practical HTTP examples for interacting with MCP tools through the Inference Gateway. These examples are useful for testing, debugging, or integrating with other systems. **Note: Always interact with MCP tools through the Inference Gateway, not directly with MCP servers.** - -### Prerequisites for HTTP Examples - -Make sure the MCP infrastructure is running with MCP tools exposed: - -```bash -npm run compose:up -``` - -**Important:** Ensure the Inference Gateway is started with `EXPOSE_MCP=true` environment variable to enable MCP endpoints. - -### 1. Health Checks - -#### Check Inference Gateway Health - -```bash -curl -X GET http://localhost:8080/health -``` - -**Response:** - -```json -{ - "status": "healthy", - "timestamp": "2025-05-28T10:30:00Z", - "version": "1.0.0" -} -``` - -### 2. MCP Tool Discovery - -#### List All Available MCP Tools via Inference Gateway - -```bash -curl -X GET http://localhost:8080/v1/mcp/tools -``` - -**Response:** - -```json -{ - "tools": [ - { - "name": "read_file", - "description": "Read content from a file", - "input_schema": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "Path to the file to read" - } - }, - "required": ["file_path"] - } - }, - { - "name": "write_file", - "description": "Write content to a file", - "input_schema": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "Path to the file to write" - }, - "content": { - "type": "string", - "description": "Content to write to the file" - } - }, - "required": ["file_path", "content"] - } - }, - { - "name": "fetch_url", - "description": "Fetch content from a URL", - "input_schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "The URL to fetch content from" - }, - "timeout": { - "type": "number", - "description": "Request timeout in milliseconds" - } - }, - "required": ["url"] - } - } - ] -} -``` - -### 3. Using MCP Tools in Chat Completions - -MCP tools are executed automatically by AI models during chat completions. You don't call them directly - instead, you include them in the `tools` array of a chat completion request. - -#### Basic Chat Completion with MCP Tools - -```bash -curl -X POST http://localhost:8080/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{ - "model": "groq/llama-3.3-70b-versatile", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant that can read files using available tools." - }, - { - "role": "user", - "content": "Can you read the contents of /tmp/example.txt?" - } - ] - }' -``` - -#### Web Scraping with MCP Tools - -```bash -curl -X POST http://localhost:8080/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{ - "model": "openai/gpt-4o", - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant that can fetch web content." - }, - { - "role": "user", - "content": "Can you fetch the content from https://httpbin.org/json?" - } - ] - }' -``` - -### 4. Streaming Chat Completions with MCP Tools - -```bash -curl -X POST http://localhost:8080/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{ - "model": "anthropic/claude-3-haiku-20240307", - "messages": [ - { - "role": "user", - "content": "Help me analyze the file /shared/data.csv by reading it first" - } - ], - "stream": true - }' -``` - -### 5. Multi-Tool Conversations - -```bash -curl -X POST http://localhost:8080/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{ - "model": "groq/mixtral-8x7b-32768", - "messages": [ - { - "role": "system", - "content": "You are a research assistant with access to file operations and web content fetching." - }, - { - "role": "user", - "content": "Research information about AI from https://openai.com and save a summary to /tmp/ai-research.txt" - } - ] - }' +docker compose -f docker-compose-agents.yml run --rm nextjs-agent # 🤖 Next.js development agent +docker compose -f docker-compose-agents.yml run --rm vite-agent # ⚡ Vite application agent +docker compose -f docker-compose-agents.yml run --rm kubernetes-agent # ☸️ Kubernetes operations agent ``` diff --git a/examples/mcp/agents/kubernetes/Dockerfile b/examples/mcp/agents/kubernetes/Dockerfile new file mode 100644 index 0000000..fc235f4 --- /dev/null +++ b/examples/mcp/agents/kubernetes/Dockerfile @@ -0,0 +1,15 @@ +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash \ + curl \ + git \ + wget \ + ca-certificates + +RUN curl -LO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/$(dpkg --print-architecture)/kubectl" \ + && install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl \ + && rm kubectl +RUN curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash +WORKDIR /app + diff --git a/examples/mcp/example-kubernetes-agent.ts b/examples/mcp/agents/kubernetes/index.ts similarity index 99% rename from examples/mcp/example-kubernetes-agent.ts rename to examples/mcp/agents/kubernetes/index.ts index 2cb0093..f2b8736 100644 --- a/examples/mcp/example-kubernetes-agent.ts +++ b/examples/mcp/agents/kubernetes/index.ts @@ -5,14 +5,14 @@ * and container orchestration using Context7 MCP tools for up-to-date K8s documentation. */ -import * as dotenv from 'dotenv'; -import * as path from 'path'; -import * as readline from 'readline'; import { InferenceGatewayClient, MessageRole, Provider, -} from '../../src/index.js'; +} from '@inference-gateway/sdk'; +import * as dotenv from 'dotenv'; +import * as path from 'path'; +import * as readline from 'readline'; dotenv.config({ path: path.join(__dirname, '.env') }); diff --git a/examples/mcp/package-lock.json b/examples/mcp/agents/kubernetes/package-lock.json similarity index 100% rename from examples/mcp/package-lock.json rename to examples/mcp/agents/kubernetes/package-lock.json diff --git a/examples/mcp/package.json b/examples/mcp/agents/kubernetes/package.json similarity index 100% rename from examples/mcp/package.json rename to examples/mcp/agents/kubernetes/package.json diff --git a/examples/mcp/agents/nextjs/Dockerfile b/examples/mcp/agents/nextjs/Dockerfile new file mode 100644 index 0000000..29566e3 --- /dev/null +++ b/examples/mcp/agents/nextjs/Dockerfile @@ -0,0 +1,10 @@ +FROM node:20-alpine + +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +EXPOSE 3000 +ENV HOST=0.0.0.0 + +CMD ["npm", "start"] diff --git a/examples/mcp/example-nextjs-agent.ts b/examples/mcp/agents/nextjs/index.ts similarity index 99% rename from examples/mcp/example-nextjs-agent.ts rename to examples/mcp/agents/nextjs/index.ts index 8a04bba..e409287 100644 --- a/examples/mcp/example-nextjs-agent.ts +++ b/examples/mcp/agents/nextjs/index.ts @@ -5,14 +5,14 @@ * using Context7 MCP tools for up-to-date documentation and library information. */ -import * as dotenv from 'dotenv'; -import * as path from 'path'; -import * as readline from 'readline'; import { InferenceGatewayClient, MessageRole, Provider, -} from '../../src/index.js'; +} from '@inference-gateway/sdk'; +import * as dotenv from 'dotenv'; +import * as path from 'path'; +import * as readline from 'readline'; dotenv.config({ path: path.join(__dirname, '.env') }); @@ -35,7 +35,7 @@ class Context7Agent { constructor() { this.config = { client: new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', + baseURL: 'http://inference-gateway:8080/v1', }), provider: (process.env.PROVIDER as Provider) || Provider.groq, model: process.env.LLM || 'llama-3.3-70b-versatile', diff --git a/examples/mcp/agents/nextjs/package-lock.json b/examples/mcp/agents/nextjs/package-lock.json new file mode 100644 index 0000000..76893b4 --- /dev/null +++ b/examples/mcp/agents/nextjs/package-lock.json @@ -0,0 +1,853 @@ +{ + "name": "mcp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@inference-gateway/sdk": "^0.7.1", + "axios": "^1.9.0", + "dotenv": "^16.5.0" + }, + "devDependencies": { + "tsx": "^4.19.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inference-gateway/sdk": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.1.tgz", + "integrity": "sha512-O6wHlmB5XmQApASaw6yhTaRHMFkSzLUl9DNGb2RYN3/0wK5Bdlymed8HCl69dbATfkEh3eXU9SiZ8FG/pww7Lg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0", + "npm": ">=10.9.0" + }, + "peerDependencies": { + "node-fetch": "^2.7.0" + }, + "peerDependenciesMeta": { + "node-fetch": { + "optional": true + } + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + } + } +} diff --git a/examples/mcp/agents/nextjs/package.json b/examples/mcp/agents/nextjs/package.json new file mode 100644 index 0000000..d82d8bc --- /dev/null +++ b/examples/mcp/agents/nextjs/package.json @@ -0,0 +1,21 @@ +{ + "name": "mcp", + "version": "1.0.0", + "description": "This example demonstrates how to use the Inference Gateway SDK with Model Context Protocol (MCP) tools in a multi-provider architecture.", + "main": "index.js", + "private": true, + "scripts": { + "start": "tsx index.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@inference-gateway/sdk": "^0.7.1", + "axios": "^1.9.0", + "dotenv": "^16.5.0" + }, + "devDependencies": { + "tsx": "^4.19.4" + } +} diff --git a/examples/mcp/agents/vite/Dockerfile b/examples/mcp/agents/vite/Dockerfile new file mode 100644 index 0000000..26ba3a5 --- /dev/null +++ b/examples/mcp/agents/vite/Dockerfile @@ -0,0 +1,10 @@ +FROM node:20-alpine + +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +EXPOSE 5173 +ENV HOST=0.0.0.0 + +CMD ["npm", "run", "dev"] diff --git a/examples/mcp/example-vite-agent.ts b/examples/mcp/agents/vite/index.ts similarity index 99% rename from examples/mcp/example-vite-agent.ts rename to examples/mcp/agents/vite/index.ts index 912c3d7..9f02526 100644 --- a/examples/mcp/example-vite-agent.ts +++ b/examples/mcp/agents/vite/index.ts @@ -5,14 +5,14 @@ * documentation and best practices using Context7 MCP tools. */ -import * as dotenv from 'dotenv'; -import * as path from 'path'; -import * as readline from 'readline'; import { InferenceGatewayClient, MessageRole, Provider, -} from '../../src/index.js'; +} from '@inference-gateway/sdk'; +import * as dotenv from 'dotenv'; +import * as path from 'path'; +import * as readline from 'readline'; dotenv.config({ path: path.join(__dirname, '.env') }); diff --git a/examples/mcp/agents/vite/package-lock.json b/examples/mcp/agents/vite/package-lock.json new file mode 100644 index 0000000..76893b4 --- /dev/null +++ b/examples/mcp/agents/vite/package-lock.json @@ -0,0 +1,853 @@ +{ + "name": "mcp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@inference-gateway/sdk": "^0.7.1", + "axios": "^1.9.0", + "dotenv": "^16.5.0" + }, + "devDependencies": { + "tsx": "^4.19.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inference-gateway/sdk": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.1.tgz", + "integrity": "sha512-O6wHlmB5XmQApASaw6yhTaRHMFkSzLUl9DNGb2RYN3/0wK5Bdlymed8HCl69dbATfkEh3eXU9SiZ8FG/pww7Lg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0", + "npm": ">=10.9.0" + }, + "peerDependencies": { + "node-fetch": "^2.7.0" + }, + "peerDependenciesMeta": { + "node-fetch": { + "optional": true + } + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + } + } +} diff --git a/examples/mcp/agents/vite/package.json b/examples/mcp/agents/vite/package.json new file mode 100644 index 0000000..dfdceb8 --- /dev/null +++ b/examples/mcp/agents/vite/package.json @@ -0,0 +1,27 @@ +{ + "name": "mcp", + "version": "1.0.0", + "description": "This example demonstrates how to use the Inference Gateway SDK with Model Context Protocol (MCP) tools in a multi-provider architecture.", + "main": "index.js", + "private": true, + "scripts": { + "clean": "rm -rf shared/next-app shared/update-check", + "example:nextjs-agent": "tsx example-nextjs-agent.ts", + "example:vite-agent": "tsx example-vite-agent.ts", + "example:kubernetes-agent": "tsx example-kubernetes-agent.ts", + "compose:up": "docker-compose up -d", + "compose:down": "docker-compose down", + "compose:logs": "docker-compose logs -f" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@inference-gateway/sdk": "^0.7.1", + "axios": "^1.9.0", + "dotenv": "^16.5.0" + }, + "devDependencies": { + "tsx": "^4.19.4" + } +} diff --git a/examples/mcp/docker-compose-agents.yml b/examples/mcp/docker-compose-agents.yml new file mode 100644 index 0000000..65fa926 --- /dev/null +++ b/examples/mcp/docker-compose-agents.yml @@ -0,0 +1,41 @@ +services: + kubernetes-agent: + build: + context: ./agents/kubernetes + dockerfile: Dockerfile + env_file: + - .env + volumes: + - shared-data:/tmp + networks: + - inference-network + + vite-agent: + build: + context: ./agents/vite + dockerfile: Dockerfile + env_file: + - .env + volumes: + - shared-data:/tmp + networks: + - inference-network + + nextjs-agent: + build: + context: ./agents/nextjs + dockerfile: Dockerfile + env_file: + - .env + volumes: + - shared-data:/tmp + networks: + - inference-network + +volumes: + shared-data: + driver: local + +networks: + inference-network: + driver: bridge diff --git a/examples/mcp/mcp-servers/README.md b/examples/mcp/mcp-servers/README.md deleted file mode 100644 index 68f80cd..0000000 --- a/examples/mcp/mcp-servers/README.md +++ /dev/null @@ -1,274 +0,0 @@ -# MCP Servers - -This directory contains Model Context Protocol (MCP) servers that demonstrate how to build and integrate custom tools with the Inference Gateway. - -## Available Servers - -### 🌐 Web Search Server (`web-search/`) - -- **Port**: 3001 -- **Tools**: `fetch_url`, `search_web`, `get_page_title` -- **Purpose**: Provides web content fetching and search capabilities -- **Features**: HTTP requests, simulated search, HTML parsing - -### 📁 Filesystem Server (`filesystem/`) - -- **Port**: 3000 -- **Tools**: `read_file`, `write_file`, `list_directory`, `create_directory`, `delete_file`, `file_info` -- **Purpose**: Safe filesystem operations within allowed directories -- **Features**: File I/O, directory management, security restrictions - -### 📚 Context7 Server (`context7/`) - -- **Port**: 3002 -- **Tools**: `resolve_library_id`, `get_library_docs`, `search_libraries` -- **Purpose**: Library documentation and context resolution capabilities -- **Features**: Library search, documentation retrieval, version management - -### 📦 NPM Server (`npm/`) - -- **Port**: 3003 -- **Tools**: `npm_run`, `npm_init_project`, `npm_install_package` -- **Purpose**: Safe npm command execution with whitelisted commands -- **Features**: Package management, project initialization, security restrictions -- **Shared Volume**: `/tmp` directory shared with filesystem server - -## Quick Start - -### Individual Server Development - -Each server can be run independently for development: - -```bash -# Web Search Server -cd web-search -npm install -npm run dev - -# Filesystem Server -cd filesystem -npm install -npm run dev - -# Context7 Server -cd context7 -npm install -npm run dev -``` - -### Docker Compose Integration - -All servers are configured to work together with the Inference Gateway: - -```bash -# From the main MCP example directory -docker-compose up -d -``` - -This will start: - -- Inference Gateway (port 8080) -- MCP Filesystem Server (port 3000) -- MCP Web Search Server (port 3001) -- MCP Context7 Server (port 3002) -- Optional: Ollama (port 11434) - -## Server Architecture - -Each MCP server follows a consistent structure: - -``` -server-name/ -├── package.json # Dependencies and scripts -├── index.js # Main server implementation -└── README.md # Server-specific documentation -``` - -### Core Endpoints - -All MCP servers implement these standard endpoints: - -- `GET /mcp` - Server information and capabilities -- `POST /mcp/tools/list` - List available tools -- `POST /mcp/tools/call` - Execute tools -- `GET /health` - Health check - -## Tool Development - -### Adding New Tools - -To add a new tool to an existing server: - -1. **Define the tool** in the `/mcp/tools/list` endpoint: - -```javascript -{ - name: 'my_new_tool', - description: 'Description of what the tool does', - inputSchema: { - type: 'object', - properties: { - param1: { - type: 'string', - description: 'Parameter description' - } - }, - required: ['param1'] - } -} -``` - -2. **Implement the handler** function: - -```javascript -async function handleMyNewTool(args, res) { - const { param1 } = args; - - try { - // Tool logic here - const result = await doSomething(param1); - - res.json({ - content: [ - { - type: 'text', - text: `Result: ${result}`, - }, - ], - }); - } catch (error) { - res.json({ - content: [ - { - type: 'text', - text: `Error: ${error.message}`, - }, - ], - }); - } -} -``` - -3. **Add the case** to the tool execution switch: - -```javascript -case 'my_new_tool': - await handleMyNewTool(args, res); - break; -``` - -### Creating New Servers - -To create a new MCP server: - -1. **Create directory structure**: - -```bash -mkdir mcp-servers/my-server -cd mcp-servers/my-server -``` - -2. **Create package.json**: - -```json -{ - "name": "mcp-my-server", - "version": "1.0.0", - "main": "index.js", - "scripts": { - "start": "node index.js", - "dev": "node --watch index.js" - }, - "dependencies": { - "express": "^4.18.2", - "cors": "^2.8.5" - } -} -``` - -3. **Implement the server** following the existing patterns -4. **Add to docker-compose.yml** for integration -5. **Update Inference Gateway** MCP_SERVERS configuration - -## Integration with Inference Gateway - -The servers are configured to work with the Inference Gateway through environment variables: - -```yaml -# In docker-compose.yml -environment: - MCP_ENABLE: 'true' - MCP_EXPOSE: 'true' - MCP_SERVERS: 'filesystem=http://mcp-filesystem:3000/mcp,web-search=http://mcp-web-search:3001/mcp' -``` - -## Security Considerations - -### Filesystem Server - -- Restricts operations to allowed directories -- Validates all file paths -- Prevents directory traversal attacks -- Implements proper error handling - -### Web Search Server - -- Includes request timeouts -- Limits response sizes -- Handles various content types safely -- Provides safe error messages - -### General Security - -- All servers run with minimal privileges -- Docker containers are isolated -- Health checks monitor server status -- Graceful shutdown handling - -## Testing - -Each server includes health check endpoints and can be tested independently: - -```bash -# Test server health -curl http://localhost:3000/health -curl http://localhost:3001/health - -# Test MCP endpoints -curl http://localhost:3000/mcp -curl -X POST http://localhost:3000/mcp/tools/list -``` - -## Monitoring - -Monitor server logs during development: - -```bash -# Follow logs for all services -docker-compose logs -f - -# Follow logs for specific service -docker-compose logs -f mcp-filesystem -docker-compose logs -f mcp-web-search -``` - -## Examples - -See the main MCP example (`../index.ts`) for complete usage examples showing how to: - -- Discover available MCP tools -- Execute filesystem operations -- Perform web searches and content fetching -- Handle tool responses and errors - -## Contributing - -When contributing new servers or tools: - -1. Follow the established patterns and conventions -2. Include comprehensive error handling -3. Add proper validation for all inputs -4. Document all tools and parameters -5. Include health check endpoints -6. Test thoroughly with the Inference Gateway -7. Update this README with new server information From bf228024164076593a9b6d814bb445e28f77ebf8 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 14:34:45 +0000 Subject: [PATCH 17/38] docs: Implement MCP Memory Server for state persistence and error recovery Signed-off-by: Eden Reich --- examples/mcp/README.md | 36 +- examples/mcp/agents/kubernetes/Dockerfile | 8 + examples/mcp/agents/kubernetes/index.ts | 35 +- examples/mcp/agents/nextjs/index.ts | 34 +- examples/mcp/agents/vite/index.ts | 34 +- examples/mcp/docker-compose.yml | 34 +- examples/mcp/mcp-servers/context7/index.js | 47 +-- examples/mcp/mcp-servers/memory/README.md | 97 +++++ examples/mcp/mcp-servers/memory/index.js | 379 +++++++++++++++++++ examples/mcp/mcp-servers/memory/package.json | 22 ++ 10 files changed, 666 insertions(+), 60 deletions(-) create mode 100644 examples/mcp/mcp-servers/memory/README.md create mode 100644 examples/mcp/mcp-servers/memory/index.js create mode 100644 examples/mcp/mcp-servers/memory/package.json diff --git a/examples/mcp/README.md b/examples/mcp/README.md index 28424a0..724578f 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -8,7 +8,37 @@ This directory contains comprehensive examples demonstrating how to use the Infe ```bash # Run interactive specialized agents -docker compose -f docker-compose-agents.yml run --rm nextjs-agent # 🤖 Next.js development agent -docker compose -f docker-compose-agents.yml run --rm vite-agent # ⚡ Vite application agent -docker compose -f docker-compose-agents.yml run --rm kubernetes-agent # ☸️ Kubernetes operations agent +docker compose -f docker-compose-agents.yml run --rm -it nextjs-agent # 🤖 Next.js development agent +docker compose -f docker-compose-agents.yml run --rm -it vite-agent # ⚡ Vite application agent +docker compose -f docker-compose-agents.yml run --rm -it kubernetes-agent # ☸️ Kubernetes operations agent ``` + +## 🧠 Memory & Error Recovery + +The **Memory MCP Server** provides persistent state management for AI agents, enabling them to recover gracefully from HTTP errors and continue from where they left off. This is particularly useful for long-running tasks that may encounter temporary network issues or API failures. + +### Key Features + +- **State Persistence**: Save arbitrary state objects with session IDs +- **Error State Recovery**: Special handling for HTTP error scenarios +- **Session Management**: List, restore, and clear saved sessions +- **File-based Storage**: Persistent storage using JSON files + +### Memory Tools Integration + +All agents (Next.js, Vite, and Kubernetes) now include memory recovery capabilities: + +1. **Save State Before Risky Operations**: Before making HTTP requests, agents save their current progress +2. **Handle Errors Gracefully**: When HTTP errors occur, agents save the error state with context +3. **Resume from Last Checkpoint**: On restart, agents check for saved state and continue from the last successful step +4. **Memory Management**: Agents can list, restore, and clear saved sessions + +### Available Memory Tools + +- `save-state`: Save current progress/state with a session ID +- `save-error-state`: Save state when HTTP errors occur for recovery +- `restore-state`: Restore previously saved state by session ID +- `list-sessions`: List all saved sessions +- `clear-session`: Remove a saved session + +All agents will automatically use these tools when encountering HTTP errors, ensuring robust error recovery and task continuation. diff --git a/examples/mcp/agents/kubernetes/Dockerfile b/examples/mcp/agents/kubernetes/Dockerfile index fc235f4..0312a97 100644 --- a/examples/mcp/agents/kubernetes/Dockerfile +++ b/examples/mcp/agents/kubernetes/Dockerfile @@ -7,9 +7,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ wget \ ca-certificates +RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ + && apt-get install -y nodejs + RUN curl -LO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/$(dpkg --print-architecture)/kubectl" \ && install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl \ && rm kubectl RUN curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + WORKDIR /app +COPY package.json ./ +RUN npm install +COPY . . +CMD ["npm", "start"] diff --git a/examples/mcp/agents/kubernetes/index.ts b/examples/mcp/agents/kubernetes/index.ts index f2b8736..b907634 100644 --- a/examples/mcp/agents/kubernetes/index.ts +++ b/examples/mcp/agents/kubernetes/index.ts @@ -87,20 +87,47 @@ You help users with **Kubernetes cluster operations and container orchestration* --- -### 🧰 CONTEXT7 TOOLS +### 🧰 AVAILABLE TOOLS -You have access to either **Real** or **Mock** Context7 tools. +You have access to several MCP tool categories: -**Real Context7 Tools (@upstash/context7-mcp):** +**Context7 Tools (@upstash/context7-mcp):** * c41_resolve-library-id: Resolve technology names to Context7-compatible IDs * c41_get-library-docs: Fetch full documentation, usage examples, and best practices +**Mock Tools (for local/demo use):** * search_libraries: Search for libraries by name or functionality * get_library_details: Fetch library metadata and features * get_documentation: Fetch usage examples and implementation patterns +**Memory Tools (for error recovery):** + +* save-state: Save current progress/state with a session ID +* save-error-state: Save state when HTTP errors occur for recovery +* restore-state: Restore previously saved state by session ID +* list-sessions: List all saved sessions +* clear-session: Remove a saved session + +**File System Tools:** + +* Available for file operations in /tmp directory + +--- + +### 🛡️ ERROR RECOVERY STRATEGY + +When encountering HTTP errors or failures: + +1. Immediately save state using save-error-state with: + - Unique session ID (e.g., "k8s-task-{timestamp}") + - Current progress/context + - Error details +2. In subsequent runs, check for existing sessions with list-sessions +3. Restore state if needed and continue from where you left off +4. Clear sessions when tasks complete successfully + --- ### 📂 FILE SYSTEM RULES @@ -458,7 +485,7 @@ If Kubernetes configurations exist: process.stdout.write(content); assistantResponse += content; }, - onMCPTool: (toolCall) => { + onTool: (toolCall: any) => { console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); try { const args = JSON.parse(toolCall.function.arguments); diff --git a/examples/mcp/agents/nextjs/index.ts b/examples/mcp/agents/nextjs/index.ts index e409287..aba9975 100644 --- a/examples/mcp/agents/nextjs/index.ts +++ b/examples/mcp/agents/nextjs/index.ts @@ -87,11 +87,11 @@ You help users create **modern, production-grade applications** by: --- -### 🧰 CONTEXT7 TOOLS +### 🧰 AVAILABLE TOOLS -You have access to either **Real** or **Mock** Context7 tools. +You have access to several MCP tool categories: -**Real Context7 Tools (@upstash/context7-mcp):** +**Context7 Tools (@upstash/context7-mcp):** * c41_resolve-library-id: Resolve technology names to Context7-compatible IDs * c41_get-library-docs: Fetch full documentation, usage examples, and best practices @@ -102,6 +102,32 @@ You have access to either **Real** or **Mock** Context7 tools. * get_library_details: Fetch library metadata and features * get_documentation: Fetch usage examples and implementation patterns +**Memory Tools (for error recovery):** + +* save-state: Save current progress/state with a session ID +* save-error-state: Save state when HTTP errors occur for recovery +* restore-state: Restore previously saved state by session ID +* list-sessions: List all saved sessions +* clear-session: Remove a saved session + +**File System Tools:** + +* Available for file operations in /tmp directory + +--- + +### 🛡️ ERROR RECOVERY STRATEGY + +When encountering HTTP errors or failures: + +1. Immediately save state using save-error-state with: + - Unique session ID (e.g., "nextjs-task-{timestamp}") + - Current progress/context + - Error details +2. In subsequent runs, check for existing sessions with list-sessions +3. Restore state if needed and continue from where you left off +4. Clear sessions when tasks complete successfully + --- ### 📂 FILE SYSTEM RULES @@ -403,7 +429,7 @@ If a Next.js project exists: process.stdout.write(content); assistantResponse += content; }, - onMCPTool: (toolCall) => { + onTool: (toolCall) => { console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); try { const args = JSON.parse(toolCall.function.arguments); diff --git a/examples/mcp/agents/vite/index.ts b/examples/mcp/agents/vite/index.ts index 9f02526..5ffd007 100644 --- a/examples/mcp/agents/vite/index.ts +++ b/examples/mcp/agents/vite/index.ts @@ -87,11 +87,11 @@ You help users create **modern, lightning-fast Vite applications** by: --- -### 🧰 CONTEXT7 TOOLS +### 🧰 AVAILABLE TOOLS -You have access to either **Real** or **Mock** Context7 tools. +You have access to several MCP tool categories: -**Real Context7 Tools (@upstash/context7-mcp):** +**Context7 Tools (@upstash/context7-mcp):** * c41_resolve-library-id: Resolve technology names to Context7-compatible IDs * c41_get-library-docs: Fetch full documentation, usage examples, and best practices @@ -102,6 +102,32 @@ You have access to either **Real** or **Mock** Context7 tools. * get_library_details: Fetch library metadata and features * get_documentation: Fetch usage examples and implementation patterns +**Memory Tools (for error recovery):** + +* save-state: Save current progress/state with a session ID +* save-error-state: Save state when HTTP errors occur for recovery +* restore-state: Restore previously saved state by session ID +* list-sessions: List all saved sessions +* clear-session: Remove a saved session + +**File System Tools:** + +* Available for file operations in /tmp directory + +--- + +### 🛡️ ERROR RECOVERY STRATEGY + +When encountering HTTP errors or failures: + +1. Immediately save state using save-error-state with: + - Unique session ID (e.g., "vite-task-{timestamp}") + - Current progress/context + - Error details +2. In subsequent runs, check for existing sessions with list-sessions +3. Restore state if needed and continue from where you left off +4. Clear sessions when tasks complete successfully + --- ### 📂 FILE SYSTEM RULES @@ -429,7 +455,7 @@ If a Vite project exists: process.stdout.write(content); assistantResponse += content; }, - onMCPTool: (toolCall) => { + onTool: (toolCall: any) => { console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); try { const args = JSON.parse(toolCall.function.arguments); diff --git a/examples/mcp/docker-compose.yml b/examples/mcp/docker-compose.yml index 29aef8d..f463130 100644 --- a/examples/mcp/docker-compose.yml +++ b/examples/mcp/docker-compose.yml @@ -10,7 +10,7 @@ services: # Enable MCP support MCP_ENABLE: 'true' MCP_EXPOSE: 'true' - MCP_SERVERS: 'http://mcp-filesystem:3000/mcp,http://mcp-web-search:3001/mcp,http://mcp-context7:3002/mcp,http://mcp-npm:3003/mcp' + MCP_SERVERS: 'http://mcp-filesystem:3000/mcp,http://mcp-web-search:3001/mcp,http://mcp-context7:3002/mcp,http://mcp-npm:3003/mcp,http://mcp-memory:3004/mcp' # Server settings SERVER_HOST: '0.0.0.0' @@ -44,6 +44,8 @@ services: condition: service_healthy mcp-npm: condition: service_healthy + mcp-memory: + condition: service_healthy networks: - inference-network healthcheck: @@ -170,6 +172,36 @@ services: start_period: 45s restart: unless-stopped + mcp-memory: + build: + context: ./mcp-servers/memory + dockerfile_inline: | + FROM node:18-alpine + WORKDIR /app + RUN apk add --no-cache curl + COPY package.json ./ + RUN npm install + COPY . . + EXPOSE 3004 + CMD ["npm", "start"] + environment: + NODE_ENV: 'production' + MCP_SERVER_NAME: 'memory' + MCP_SERVER_VERSION: '1.0.0' + PORT: '3004' + MEMORY_DIR: '/tmp/memory' + networks: + - inference-network + volumes: + - ./shared:/tmp/memory + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:3004/health'] + interval: 30s + timeout: 10s + retries: 5 + start_period: 45s + restart: unless-stopped + # # Optional: Ollama for local models # ollama: # image: ollama/ollama:latest diff --git a/examples/mcp/mcp-servers/context7/index.js b/examples/mcp/mcp-servers/context7/index.js index 543d046..d7b6204 100644 --- a/examples/mcp/mcp-servers/context7/index.js +++ b/examples/mcp/mcp-servers/context7/index.js @@ -11,15 +11,11 @@ import cors from 'cors'; import { spawn } from 'node:child_process'; import { randomUUID } from 'node:crypto'; -// Express app for HTTP transport const app = express(); app.use(express.json()); app.use(cors()); -// Map to store MCP sessions const mcpSessions = new Map(); - -// Context7 process instances cache const context7Processes = new Map(); /** @@ -42,7 +38,6 @@ class Context7Process { this.readyPromise = new Promise((resolve, reject) => { console.info('🚀 Spawning Context7 MCP server...'); - // Spawn the real Context7 MCP server this.process = spawn('npx', ['-y', '@upstash/context7-mcp@latest'], { stdio: ['pipe', 'pipe', 'pipe'], env: { @@ -53,7 +48,6 @@ class Context7Process { let buffer = ''; - // Handle stdout - MCP protocol messages this.process.stdout.on('data', (data) => { buffer += data.toString(); const lines = buffer.split('\n'); @@ -83,24 +77,21 @@ class Context7Process { } }); - // Handle process exit this.process.on('exit', (code) => { console.info(`🔚 Context7 process exited with code ${code}`); this.isReady = false; this.process = null; - // Reject all pending requests for (const [, { reject }] of this.pendingRequests) { reject(new Error('Context7 process terminated')); } this.pendingRequests.clear(); }); - // Handle errors this.process.on('error', (error) => { console.error('❌ Context7 process error:', error); reject(error); - }); // Initialize the MCP session + }); globalThis.setTimeout(() => { this.sendInitialize(); }, 2000); @@ -127,12 +118,10 @@ class Context7Process { }, }; - // Track this request properly const messageId = initMessage.id; const initPromise = new Promise((resolve, reject) => { this.pendingRequests.set(messageId, { resolve, reject }); - // Set timeout for initialization globalThis.setTimeout(() => { if (this.pendingRequests.has(messageId)) { this.pendingRequests.delete(messageId); @@ -143,12 +132,10 @@ class Context7Process { this.sendMessage(initMessage); - // Handle the initialization response initPromise .then(() => { console.info('✅ Context7 initialized successfully'); this.isReady = true; - // Send initialized notification after successful init globalThis.setTimeout(() => { this.sendMessage({ jsonrpc: '2.0', @@ -181,7 +168,6 @@ class Context7Process { JSON.stringify(message, null, 2) ); - // Handle responses to our requests if (message.id && this.pendingRequests.has(message.id)) { const { resolve, reject } = this.pendingRequests.get(message.id); this.pendingRequests.delete(message.id); @@ -197,7 +183,6 @@ class Context7Process { return; } - // Special handling for initialization - Context7 doesn't send notifications/initialized if ( message.result && message.result.serverInfo && @@ -210,7 +195,6 @@ class Context7Process { return; } - // Handle specific notifications and responses switch (message.method) { case 'notifications/initialized': console.info('✅ Context7 initialized notification received'); @@ -249,13 +233,12 @@ class Context7Process { return new Promise((resolve, reject) => { this.pendingRequests.set(messageId, { resolve, reject }); - // Set timeout for the request globalThis.setTimeout(() => { if (this.pendingRequests.has(messageId)) { this.pendingRequests.delete(messageId); reject(new Error('Context7 request timeout')); } - }, 30000); // 30 second timeout + }, 30000); this.sendMessage(message); }); @@ -270,12 +253,10 @@ class Context7Process { params: {}, }; - // Track this request properly const messageId = toolsListMessage.id; const toolsPromise = new Promise((resolve, reject) => { this.pendingRequests.set(messageId, { resolve, reject }); - // Set timeout for tools list request globalThis.setTimeout(() => { if (this.pendingRequests.has(messageId)) { this.pendingRequests.delete(messageId); @@ -286,7 +267,6 @@ class Context7Process { this.sendMessage(toolsListMessage); - // Handle the tools list response toolsPromise .then((result) => { console.info( @@ -330,7 +310,6 @@ class Context7Process { console.info('🔄 Terminating Context7 process...'); this.process.kill('SIGTERM'); - // Force kill after 5 seconds globalThis.setTimeout(() => { if (this.process) { console.info('🔪 Force killing Context7 process...'); @@ -349,7 +328,6 @@ function getContext7Process(sessionId = 'default') { const process = new Context7Process(); context7Processes.set(sessionId, process); - // Clean up after 10 minutes of inactivity globalThis.setTimeout( () => { if (context7Processes.has(sessionId)) { @@ -659,14 +637,12 @@ async function handleMcpRequest(request) { * Setup MCP endpoints for proper Model Context Protocol communication */ function setupSessionRoutes() { - // Handle POST requests for MCP communication app.post('/mcp', async (req, res) => { try { console.info('📨 MCP POST request received:'); console.info(' Headers: %s', JSON.stringify(req.headers, null, 2)); console.info(' Body: %s', JSON.stringify(req.body, null, 2)); - // Validate request body if (!req.body || typeof req.body !== 'object') { return res.status(400).json({ jsonrpc: '2.0', @@ -689,7 +665,6 @@ function setupSessionRoutes() { }); } - // Get or create session const sessionId = req.headers['mcp-session-id'] || randomUUID(); if (!mcpSessions.has(sessionId)) { @@ -698,7 +673,6 @@ function setupSessionRoutes() { }); console.info(`🎯 MCP session created: ${sessionId}`); - // Set session cleanup timer globalThis.setTimeout( () => { if (mcpSessions.has(sessionId)) { @@ -707,13 +681,11 @@ function setupSessionRoutes() { } }, 10 * 60 * 1000 - ); // 10 minutes + ); } - // Handle the MCP request const response = await handleMcpRequest(req.body); - // Set session ID header in response res.setHeader('mcp-session-id', sessionId); res.json(response); } catch (error) { @@ -733,7 +705,6 @@ function setupSessionRoutes() { } }); - // Handle GET requests for SSE (server-to-client notifications) app.get('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id']; @@ -747,7 +718,6 @@ function setupSessionRoutes() { }); } - // Set up SSE stream res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', @@ -758,7 +728,6 @@ function setupSessionRoutes() { res.write('data: {"type":"connected"}\n\n'); - // Keep connection alive const keepAlive = globalThis.setInterval(() => { res.write('data: {"type":"ping"}\n\n'); }, 30000); @@ -768,7 +737,6 @@ function setupSessionRoutes() { }); }); - // Handle DELETE requests for session termination app.delete('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id']; @@ -782,7 +750,6 @@ function setupSessionRoutes() { }); } - // Clean up session mcpSessions.delete(sessionId); console.info(`🗑️ Session terminated: ${sessionId}`); @@ -793,7 +760,6 @@ function setupSessionRoutes() { }); } -// Health check endpoint app.get('/health', (_req, res) => { const healthStatus = { status: 'healthy', @@ -815,7 +781,6 @@ async function startServer() { const PORT = process.env.PORT || 3002; const host = process.env.HOST || '0.0.0.0'; - // Set up session routes setupSessionRoutes(); app.listen(PORT, host, () => { @@ -842,14 +807,11 @@ async function startServer() { }); } -// Graceful shutdown process.on('SIGTERM', () => { console.info('🔄 Received SIGTERM, shutting down gracefully'); - // Clear all MCP sessions mcpSessions.clear(); - // Terminate all Context7 processes context7Processes.forEach((proc) => { proc.terminate(); }); @@ -860,10 +822,8 @@ process.on('SIGTERM', () => { process.on('SIGINT', () => { console.info('🔄 Received SIGINT, shutting down gracefully'); - // Clear all MCP sessions mcpSessions.clear(); - // Terminate all Context7 processes context7Processes.forEach((proc) => { proc.terminate(); }); @@ -871,7 +831,6 @@ process.on('SIGINT', () => { process.exit(0); }); -// Start the server startServer().catch((error) => { console.error('💥 Failed to start server:', error); process.exit(1); diff --git a/examples/mcp/mcp-servers/memory/README.md b/examples/mcp/mcp-servers/memory/README.md new file mode 100644 index 0000000..47ae5e4 --- /dev/null +++ b/examples/mcp/mcp-servers/memory/README.md @@ -0,0 +1,97 @@ +# MCP Memory Server + +A Model Context Protocol (MCP) server that provides memory persistence capabilities for AI agents. This server helps agents save their state when HTTP errors occur, allowing them to continue from where they left off in subsequent iterations. + +## Features + +- **State Persistence**: Save arbitrary state objects with session IDs +- **Error State Recovery**: Specially handle saving state when HTTP errors occur +- **Session Management**: List, restore, and clear saved sessions +- **File-based Storage**: Persistent storage using JSON files + +## Tools + +### `save-state` + +Saves the current state for a session. + +**Parameters:** + +- `sessionId` (string): Unique session identifier +- `state` (object): State object to persist +- `context` (string, optional): Context description + +### `save-error-state` + +Saves state along with error information when an HTTP error occurs. + +**Parameters:** + +- `sessionId` (string): Unique session identifier +- `state` (object): State object to persist +- `error` (object): Error information (message, code, status, url) +- `context` (string, optional): Context description + +### `restore-state` + +Restores the saved state for a session. + +**Parameters:** + +- `sessionId` (string): Unique session identifier + +### `list-sessions` + +Lists all saved sessions with their metadata. + +### `clear-session` + +Removes a saved session. + +**Parameters:** + +- `sessionId` (string): Unique session identifier + +## Environment Variables + +- `MEMORY_DIR`: Directory for storing memory files (default: `/tmp/memory`) +- `PORT`: Server port (default: `3004`) + +## Usage Example + +```javascript +// Save state before making an HTTP request +await saveState({ + sessionId: 'agent-task-123', + state: { + currentStep: 'api-call', + url: 'https://api.example.com/data', + payload: { param: 'value' }, + retryCount: 0, + }, + context: 'About to make API call to fetch user data', +}); + +// If HTTP error occurs, save error state +await saveErrorState({ + sessionId: 'agent-task-123', + state: { + currentStep: 'api-call-failed', + url: 'https://api.example.com/data', + payload: { param: 'value' }, + retryCount: 1, + }, + error: { + message: 'HTTP 500 Internal Server Error', + status: 500, + url: 'https://api.example.com/data', + }, + context: 'API call failed, need to retry', +}); + +// In next iteration, restore state +const restoredData = await restoreState({ + sessionId: 'agent-task-123', +}); +// Continue from where we left off... +``` diff --git a/examples/mcp/mcp-servers/memory/index.js b/examples/mcp/mcp-servers/memory/index.js new file mode 100644 index 0000000..8b7c618 --- /dev/null +++ b/examples/mcp/mcp-servers/memory/index.js @@ -0,0 +1,379 @@ +/** + * MCP Memory Server + * + * This is a Model Context Protocol (MCP) server that provides memory + * operations for persisting state when HTTP errors occur. It allows + * agents to save their progress and continue from where they left off + * after encountering errors. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import express from 'express'; +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; +import { promises as fs } from 'node:fs'; +import path from 'node:path'; + +const app = express(); +app.use(express.json()); + +const transports = {}; + +const memoryDir = process.env.MEMORY_DIR || '/tmp/memory'; + +console.info('Memory directory:', memoryDir); + +/** + * Ensure memory directory exists + */ +async function ensureMemoryDir() { + try { + await fs.mkdir(memoryDir, { recursive: true }); + } catch (error) { + console.error('Failed to create memory directory:', error); + } +} + +/** + * Get memory file path for a given session + */ +function getMemoryPath(sessionId) { + return path.join(memoryDir, `${sessionId}.json`); +} + +/** + * Create and configure the MCP server + */ +function createMcpServer() { + const mcpServer = new McpServer({ + name: 'memory', + version: '1.0.0', + }); + + mcpServer.tool( + 'save-state', + { + sessionId: z.string().describe('Unique session identifier'), + state: z.object({}).passthrough().describe('State object to persist'), + context: z.string().optional().describe('Optional context description'), + }, + async ({ sessionId, state, context }) => { + try { + await ensureMemoryDir(); + + const memoryData = { + sessionId, + state, + context, + timestamp: new Date().toISOString(), + lastError: null, + }; + + const memoryPath = getMemoryPath(sessionId); + await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); + + return { + content: [ + { + type: 'text', + text: `State saved successfully for session: ${sessionId}`, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Failed to save state: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + mcpServer.tool( + 'save-error-state', + { + sessionId: z.string().describe('Unique session identifier'), + state: z.object({}).passthrough().describe('State object to persist'), + error: z + .object({ + message: z.string(), + code: z.number().optional(), + status: z.number().optional(), + url: z.string().optional(), + }) + .describe('Error information'), + context: z.string().optional().describe('Optional context description'), + }, + async ({ sessionId, state, error, context }) => { + try { + await ensureMemoryDir(); + + const memoryData = { + sessionId, + state, + context, + timestamp: new Date().toISOString(), + lastError: { + ...error, + timestamp: new Date().toISOString(), + }, + }; + + const memoryPath = getMemoryPath(sessionId); + await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); + + return { + content: [ + { + type: 'text', + text: `Error state saved successfully for session: ${sessionId}. Error: ${error.message}`, + }, + ], + }; + } catch (saveError) { + return { + content: [ + { + type: 'text', + text: `Failed to save error state: ${saveError.message}`, + }, + ], + isError: true, + }; + } + } + ); + + mcpServer.tool( + 'restore-state', + { + sessionId: z.string().describe('Unique session identifier'), + }, + async ({ sessionId }) => { + try { + const memoryPath = getMemoryPath(sessionId); + + try { + const memoryData = JSON.parse(await fs.readFile(memoryPath, 'utf8')); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + sessionId: memoryData.sessionId, + state: memoryData.state, + context: memoryData.context, + timestamp: memoryData.timestamp, + lastError: memoryData.lastError, + hasError: !!memoryData.lastError, + }, + null, + 2 + ), + }, + ], + }; + } catch (readError) { + if (readError.code === 'ENOENT') { + return { + content: [ + { + type: 'text', + text: `No saved state found for session: ${sessionId}`, + }, + ], + }; + } + throw readError; + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Failed to restore state: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + mcpServer.tool('list-sessions', {}, async () => { + try { + await ensureMemoryDir(); + const files = await fs.readdir(memoryDir); + const jsonFiles = files.filter((file) => file.endsWith('.json')); + + const sessions = []; + for (const file of jsonFiles) { + try { + const sessionId = path.basename(file, '.json'); + const memoryData = JSON.parse( + await fs.readFile(path.join(memoryDir, file), 'utf8') + ); + + sessions.push({ + sessionId, + context: memoryData.context, + timestamp: memoryData.timestamp, + hasError: !!memoryData.lastError, + lastError: memoryData.lastError?.message, + }); + } catch (readError) { + console.warn( + `Failed to read session file ${file}:`, + readError.message + ); + } + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ sessions }, null, 2), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Failed to list sessions: ${error.message}`, + }, + ], + isError: true, + }; + } + }); + + mcpServer.tool( + 'clear-session', + { + sessionId: z.string().describe('Unique session identifier'), + }, + async ({ sessionId }) => { + try { + const memoryPath = getMemoryPath(sessionId); + await fs.unlink(memoryPath); + + return { + content: [ + { + type: 'text', + text: `Session cleared successfully: ${sessionId}`, + }, + ], + }; + } catch (error) { + if (error.code === 'ENOENT') { + return { + content: [ + { + type: 'text', + text: `No session found to clear: ${sessionId}`, + }, + ], + }; + } + + return { + content: [ + { + type: 'text', + text: `Failed to clear session: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + return mcpServer; +} + +/** + * Handle MCP requests + */ +app.post('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] || randomUUID(); + + try { + let transport = transports[sessionId]; + + if (!transport) { + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => sessionId, + }); + + transports[sessionId] = transport; + + transport.onclose = () => { + delete transports[sessionId]; + }; + + const mcpServer = createMcpServer(); + await mcpServer.connect(transport); + } + + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } +}); + +// Health check endpoint +app.get('/health', (req, res) => { + res.status(200).json({ status: 'healthy' }); +}); + +// Method not allowed handlers +app.get('/mcp', (req, res) => { + res.status(405).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed', + }, + id: null, + }); +}); + +app.delete('/mcp', (req, res) => { + res.status(405).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed', + }, + id: null, + }); +}); + +// Start the server +const PORT = process.env.PORT || 3004; +app.listen(PORT, () => { + console.log(`MCP Memory Server listening on port ${PORT}`); +}); diff --git a/examples/mcp/mcp-servers/memory/package.json b/examples/mcp/mcp-servers/memory/package.json new file mode 100644 index 0000000..df4a9f3 --- /dev/null +++ b/examples/mcp/mcp-servers/memory/package.json @@ -0,0 +1,22 @@ +{ + "name": "mcp-memory-server", + "version": "1.0.0", + "description": "MCP Memory Server for persisting state on HTTP errors", + "type": "module", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "cors": "^2.8.5", + "express": "^4.18.2", + "zod": "^3.22.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "author": "Inference Gateway Team", + "license": "MIT" +} From b49a1fd73027989a1398853a733ab075cd623aef Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 14:37:48 +0000 Subject: [PATCH 18/38] docs(examples-fix): Update @inference-gateway/sdk to version 0.7.2 and rename tool callback to onMCPTool Signed-off-by: Eden Reich --- examples/mcp/agents/kubernetes/index.ts | 2 +- examples/mcp/agents/kubernetes/package-lock.json | 8 ++++---- examples/mcp/agents/kubernetes/package.json | 2 +- examples/mcp/agents/nextjs/index.ts | 2 +- examples/mcp/agents/nextjs/package-lock.json | 8 ++++---- examples/mcp/agents/nextjs/package.json | 2 +- examples/mcp/agents/vite/index.ts | 2 +- examples/mcp/agents/vite/package-lock.json | 8 ++++---- examples/mcp/agents/vite/package.json | 2 +- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/mcp/agents/kubernetes/index.ts b/examples/mcp/agents/kubernetes/index.ts index b907634..0ea3e45 100644 --- a/examples/mcp/agents/kubernetes/index.ts +++ b/examples/mcp/agents/kubernetes/index.ts @@ -485,7 +485,7 @@ If Kubernetes configurations exist: process.stdout.write(content); assistantResponse += content; }, - onTool: (toolCall: any) => { + onMCPTool: (toolCall: any) => { console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); try { const args = JSON.parse(toolCall.function.arguments); diff --git a/examples/mcp/agents/kubernetes/package-lock.json b/examples/mcp/agents/kubernetes/package-lock.json index 76893b4..f264dac 100644 --- a/examples/mcp/agents/kubernetes/package-lock.json +++ b/examples/mcp/agents/kubernetes/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.1", + "@inference-gateway/sdk": "^0.7.2", "axios": "^1.9.0", "dotenv": "^16.5.0" }, @@ -443,9 +443,9 @@ } }, "node_modules/@inference-gateway/sdk": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.1.tgz", - "integrity": "sha512-O6wHlmB5XmQApASaw6yhTaRHMFkSzLUl9DNGb2RYN3/0wK5Bdlymed8HCl69dbATfkEh3eXU9SiZ8FG/pww7Lg==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.2.tgz", + "integrity": "sha512-oQRsGeY0L71C+H1JIyskslpenfS9GbCGvNBeSa5Twga/kgxG+iKGEb9+F7+Jc59ZyrHdMS5bRf3StRRZ4qci6Q==", "license": "MIT", "engines": { "node": ">=22.12.0", diff --git a/examples/mcp/agents/kubernetes/package.json b/examples/mcp/agents/kubernetes/package.json index dfdceb8..293a4fa 100644 --- a/examples/mcp/agents/kubernetes/package.json +++ b/examples/mcp/agents/kubernetes/package.json @@ -17,7 +17,7 @@ "author": "", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.1", + "@inference-gateway/sdk": "^0.7.2", "axios": "^1.9.0", "dotenv": "^16.5.0" }, diff --git a/examples/mcp/agents/nextjs/index.ts b/examples/mcp/agents/nextjs/index.ts index aba9975..927258c 100644 --- a/examples/mcp/agents/nextjs/index.ts +++ b/examples/mcp/agents/nextjs/index.ts @@ -429,7 +429,7 @@ If a Next.js project exists: process.stdout.write(content); assistantResponse += content; }, - onTool: (toolCall) => { + onMCPTool: (toolCall) => { console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); try { const args = JSON.parse(toolCall.function.arguments); diff --git a/examples/mcp/agents/nextjs/package-lock.json b/examples/mcp/agents/nextjs/package-lock.json index 76893b4..f264dac 100644 --- a/examples/mcp/agents/nextjs/package-lock.json +++ b/examples/mcp/agents/nextjs/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.1", + "@inference-gateway/sdk": "^0.7.2", "axios": "^1.9.0", "dotenv": "^16.5.0" }, @@ -443,9 +443,9 @@ } }, "node_modules/@inference-gateway/sdk": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.1.tgz", - "integrity": "sha512-O6wHlmB5XmQApASaw6yhTaRHMFkSzLUl9DNGb2RYN3/0wK5Bdlymed8HCl69dbATfkEh3eXU9SiZ8FG/pww7Lg==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.2.tgz", + "integrity": "sha512-oQRsGeY0L71C+H1JIyskslpenfS9GbCGvNBeSa5Twga/kgxG+iKGEb9+F7+Jc59ZyrHdMS5bRf3StRRZ4qci6Q==", "license": "MIT", "engines": { "node": ">=22.12.0", diff --git a/examples/mcp/agents/nextjs/package.json b/examples/mcp/agents/nextjs/package.json index d82d8bc..245d1f7 100644 --- a/examples/mcp/agents/nextjs/package.json +++ b/examples/mcp/agents/nextjs/package.json @@ -11,7 +11,7 @@ "author": "", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.1", + "@inference-gateway/sdk": "^0.7.2", "axios": "^1.9.0", "dotenv": "^16.5.0" }, diff --git a/examples/mcp/agents/vite/index.ts b/examples/mcp/agents/vite/index.ts index 5ffd007..e908cc9 100644 --- a/examples/mcp/agents/vite/index.ts +++ b/examples/mcp/agents/vite/index.ts @@ -455,7 +455,7 @@ If a Vite project exists: process.stdout.write(content); assistantResponse += content; }, - onTool: (toolCall: any) => { + onMCPTool: (toolCall: any) => { console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); try { const args = JSON.parse(toolCall.function.arguments); diff --git a/examples/mcp/agents/vite/package-lock.json b/examples/mcp/agents/vite/package-lock.json index 76893b4..f264dac 100644 --- a/examples/mcp/agents/vite/package-lock.json +++ b/examples/mcp/agents/vite/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.1", + "@inference-gateway/sdk": "^0.7.2", "axios": "^1.9.0", "dotenv": "^16.5.0" }, @@ -443,9 +443,9 @@ } }, "node_modules/@inference-gateway/sdk": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.1.tgz", - "integrity": "sha512-O6wHlmB5XmQApASaw6yhTaRHMFkSzLUl9DNGb2RYN3/0wK5Bdlymed8HCl69dbATfkEh3eXU9SiZ8FG/pww7Lg==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.2.tgz", + "integrity": "sha512-oQRsGeY0L71C+H1JIyskslpenfS9GbCGvNBeSa5Twga/kgxG+iKGEb9+F7+Jc59ZyrHdMS5bRf3StRRZ4qci6Q==", "license": "MIT", "engines": { "node": ">=22.12.0", diff --git a/examples/mcp/agents/vite/package.json b/examples/mcp/agents/vite/package.json index dfdceb8..293a4fa 100644 --- a/examples/mcp/agents/vite/package.json +++ b/examples/mcp/agents/vite/package.json @@ -17,7 +17,7 @@ "author": "", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.1", + "@inference-gateway/sdk": "^0.7.2", "axios": "^1.9.0", "dotenv": "^16.5.0" }, From 857497082e0fc30a5ea8ed31f29a5051400d7a25 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 14:46:13 +0000 Subject: [PATCH 19/38] docs(examples): Add delete_directory tool with optional recursive deletion to MCP filesystem server Signed-off-by: Eden Reich --- examples/mcp/mcp-servers/filesystem/index.js | 67 +++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/examples/mcp/mcp-servers/filesystem/index.js b/examples/mcp/mcp-servers/filesystem/index.js index 142f790..844cc12 100644 --- a/examples/mcp/mcp-servers/filesystem/index.js +++ b/examples/mcp/mcp-servers/filesystem/index.js @@ -320,6 +320,70 @@ function createMcpServer() { } ); + mcpServer.tool( + 'delete_directory', + { + path: z.string().describe('The directory path to delete'), + recursive: z + .boolean() + .optional() + .describe('Whether to delete recursively (default: false)'), + }, + async ({ path: dirPath, recursive = false }) => { + if (!isPathAllowed(dirPath)) { + throw new Error( + `Access denied: ${dirPath} is not in allowed directories` + ); + } + + try { + console.info( + `Deleting directory: ${dirPath} (recursive: ${recursive})` + ); + + if (recursive) { + await fs.rm(dirPath, { recursive: true, force: true }); + } else { + await fs.rmdir(dirPath); + } + + return { + content: [ + { + type: 'text', + text: `Successfully deleted directory: ${dirPath}`, + }, + ], + }; + } catch (error) { + console.error(`Failed to delete directory ${dirPath}:`, error.message); + + let errorMessage = `Failed to delete directory: ${dirPath}\n`; + if (error.code === 'ENOENT') { + errorMessage += 'Directory does not exist'; + } else if (error.code === 'EACCES') { + errorMessage += 'Permission denied'; + } else if (error.code === 'ENOTDIR') { + errorMessage += 'Path is not a directory'; + } else if (error.code === 'ENOTEMPTY') { + errorMessage += + 'Directory is not empty (use recursive option to delete non-empty directories)'; + } else { + errorMessage += error.message; + } + + return { + content: [ + { + type: 'text', + text: errorMessage, + }, + ], + }; + } + } + ); + mcpServer.tool( 'file_info', { @@ -507,7 +571,8 @@ async function startServer() { console.info(' - list_directory - List directory contents'); console.info(' - create_directory - Create a new directory'); console.info(' - delete_file - Delete a file'); - console.info(' - move_file - Move/rename a file'); + console.info(' - delete_directory - Delete a directory'); + console.info(' - file_info - Get file or directory information'); console.info('Allowed directories:', allowedDirectories); console.info('MCP Filesystem server ready for connections'); From 31881d200a6c07e2930a1e4cede31d1192339ae7 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 14:54:31 +0000 Subject: [PATCH 20/38] fix: Rename Context7Agent to NextJSAgent and update related messages Signed-off-by: Eden Reich --- examples/mcp/agents/nextjs/index.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/mcp/agents/nextjs/index.ts b/examples/mcp/agents/nextjs/index.ts index 927258c..739ebab 100644 --- a/examples/mcp/agents/nextjs/index.ts +++ b/examples/mcp/agents/nextjs/index.ts @@ -1,5 +1,5 @@ /** - * Interactive Context7 Agent + * Interactive NextJS Agent * * This agent allows users to interactively request app development assistance * using Context7 MCP tools for up-to-date documentation and library information. @@ -28,7 +28,7 @@ interface AgentConfig { retryDelayMs: number; } -class Context7Agent { +class NextJSAgent { private config: AgentConfig; private rl: readline.Interface; @@ -197,7 +197,7 @@ If a Next.js project exists: async initialize(): Promise { console.log( - `🚀 Context7 Interactive Agent initialized using ${this.config.model} on ${this.config.provider}\n` + `🚀 NextJS Interactive Agent initialized using ${this.config.model} on ${this.config.provider}\n` ); let attempt = 0; @@ -285,7 +285,7 @@ If a Next.js project exists: } private showWelcomeMessage(): void { - console.log('🤖 Welcome to Context7 Interactive Development Agent!'); + console.log('🤖 Welcome to NextJS Interactive Development Agent!'); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log( '\n💡 I can help you create modern applications using the latest technologies.' @@ -346,7 +346,7 @@ If a Next.js project exists: switch (command) { case 'exit': case 'quit': - console.log('\n👋 Thank you for using Context7 Agent! Goodbye!'); + console.log('\n👋 Thank you for using NextJS Agent! Goodbye!'); this.rl.close(); process.exit(0); return true; @@ -420,7 +420,7 @@ If a Next.js project exists: }, { onOpen: () => { - console.log('🔗 Starting development session with Context7...\n'); + console.log('🔗 Starting development session with NextJS Agent...\n'); }, onReasoning: (reasoning) => { console.log(`\n🤔 Agent Reasoning: ${reasoning}`); @@ -480,17 +480,17 @@ If a Next.js project exists: } } -async function runContext7Agent(): Promise { - const agent = new Context7Agent(); +async function runNextJSAgent(): Promise { + const agent = new NextJSAgent(); process.on('SIGINT', async () => { - console.log('\n\n👋 Shutting down Context7 Agent...'); + console.log('\n\n👋 Shutting down NextJS Agent...'); await agent.shutdown(); process.exit(0); }); process.on('SIGTERM', async () => { - console.log('\n\n👋 Shutting down Context7 Agent...'); + console.log('\n\n👋 Shutting down NextJS Agent...'); await agent.shutdown(); process.exit(0); }); @@ -498,8 +498,8 @@ async function runContext7Agent(): Promise { await agent.initialize(); } -if (require.main === module || process.argv[1].endsWith('context7-agent.ts')) { - runContext7Agent().catch(console.error); +if (require.main === module || process.argv[1].endsWith('index.ts')) { + runNextJSAgent().catch(console.error); } -export { Context7Agent, runContext7Agent }; +export { NextJSAgent, runNextJSAgent }; From 2dc589305742e4f8b18d09de59bb263537a39424 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 14:55:10 +0000 Subject: [PATCH 21/38] fix: Update entry point check for Kubernetes and Vite agents to use 'index.ts' Signed-off-by: Eden Reich --- examples/mcp/agents/kubernetes/index.ts | 5 +---- examples/mcp/agents/vite/index.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/mcp/agents/kubernetes/index.ts b/examples/mcp/agents/kubernetes/index.ts index 0ea3e45..6babd1a 100644 --- a/examples/mcp/agents/kubernetes/index.ts +++ b/examples/mcp/agents/kubernetes/index.ts @@ -555,10 +555,7 @@ async function runKubernetesAgent(): Promise { await agent.initialize(); } -if ( - require.main === module || - process.argv[1].endsWith('kubernetes-agent.ts') -) { +if (require.main === module || process.argv[1].endsWith('index.ts')) { runKubernetesAgent().catch(console.error); } diff --git a/examples/mcp/agents/vite/index.ts b/examples/mcp/agents/vite/index.ts index e908cc9..7fc2166 100644 --- a/examples/mcp/agents/vite/index.ts +++ b/examples/mcp/agents/vite/index.ts @@ -524,7 +524,7 @@ async function runViteAgent(): Promise { await agent.initialize(); } -if (require.main === module || process.argv[1].endsWith('vite-agent.ts')) { +if (require.main === module || process.argv[1].endsWith('index.ts')) { runViteAgent().catch(console.error); } From 4dbc8ba075d0fa587376c0bb43d57afeba52e3b8 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 14:56:20 +0000 Subject: [PATCH 22/38] docs: Enhance README with additional instructions for spinning up the Inference Gateway and MCP Server Signed-off-by: Eden Reich --- examples/mcp/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/mcp/README.md b/examples/mcp/README.md index 724578f..4cc1807 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -4,8 +4,16 @@ This directory contains comprehensive examples demonstrating how to use the Infe ## 🚀 Quick Start +### Spin up the Inference Gateway and the MCP Server + +```bash +docker compose -f docker-compose.yml up --build +``` + ### Run Specific Examples +On another terminal, you can run specific examples using the Inference Gateway: + ```bash # Run interactive specialized agents docker compose -f docker-compose-agents.yml run --rm -it nextjs-agent # 🤖 Next.js development agent From 8e06d8f0c46918cdb4588ba71eb9142cc5a05e70 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 17:32:04 +0000 Subject: [PATCH 23/38] refactor: Enhance MCP Web Search server with structured logging and error handling - Introduced a standardized Winston logger for consistent logging across the MCP server. - Added middleware for logging incoming requests and their completion details. - Enhanced error handling in tools, providing structured error responses. - Implemented HTML content extraction using Cheerio, with options for plain text extraction. - Updated the fetch_url tool to log detailed fetch operations and handle various content types. - Improved logging for web search and page title extraction tools. - Added health check endpoint logging. - Updated package dependencies to include Winston for logging. Signed-off-by: Eden Reich --- examples/mcp/agents/kubernetes/index.ts | 4 + examples/mcp/agents/nextjs/index.ts | 514 ++++- examples/mcp/agents/vite/index.ts | 52 + examples/mcp/docker-compose.yml | 4 +- examples/mcp/mcp-servers/context7/index.js | 315 +-- examples/mcp/mcp-servers/context7/logger.js | 97 + .../mcp-servers/context7/package-lock.json | 260 ++- .../mcp/mcp-servers/context7/package.json | 1 + examples/mcp/mcp-servers/filesystem/index.js | 131 +- examples/mcp/mcp-servers/filesystem/logger.js | 97 + .../mcp-servers/filesystem/package-lock.json | 259 +++ .../mcp/mcp-servers/filesystem/package.json | 1 + examples/mcp/mcp-servers/memory/index.js | 10 + examples/mcp/mcp-servers/memory/logger.js | 97 + .../mcp/mcp-servers/memory/package-lock.json | 1692 +++++++++++++++++ examples/mcp/mcp-servers/memory/package.json | 1 + examples/mcp/mcp-servers/npm/index.js | 162 +- examples/mcp/mcp-servers/npm/logger.js | 97 + .../mcp/mcp-servers/npm/package-lock.json | 1692 +++++++++++++++++ examples/mcp/mcp-servers/npm/package.json | 1 + .../mcp/mcp-servers/web-search/index-http.js | 331 ---- examples/mcp/mcp-servers/web-search/index.js | 268 ++- examples/mcp/mcp-servers/web-search/logger.js | 97 + .../mcp-servers/web-search/package-lock.json | 259 +++ .../mcp/mcp-servers/web-search/package.json | 1 + 25 files changed, 5766 insertions(+), 677 deletions(-) create mode 100644 examples/mcp/mcp-servers/context7/logger.js create mode 100644 examples/mcp/mcp-servers/filesystem/logger.js create mode 100644 examples/mcp/mcp-servers/memory/logger.js create mode 100644 examples/mcp/mcp-servers/memory/package-lock.json create mode 100644 examples/mcp/mcp-servers/npm/logger.js create mode 100644 examples/mcp/mcp-servers/npm/package-lock.json delete mode 100644 examples/mcp/mcp-servers/web-search/index-http.js create mode 100644 examples/mcp/mcp-servers/web-search/logger.js diff --git a/examples/mcp/agents/kubernetes/index.ts b/examples/mcp/agents/kubernetes/index.ts index 6babd1a..596853e 100644 --- a/examples/mcp/agents/kubernetes/index.ts +++ b/examples/mcp/agents/kubernetes/index.ts @@ -26,6 +26,8 @@ interface AgentConfig { conversationHistory: Array<{ role: MessageRole; content: string }>; maxRetries: number; retryDelayMs: number; + iterationCount: number; + totalTokensUsed: number; } class KubernetesAgent { @@ -42,6 +44,8 @@ class KubernetesAgent { conversationHistory: [], maxRetries: 3, retryDelayMs: 60000, + iterationCount: 0, + totalTokensUsed: 0, }; this.rl = readline.createInterface({ diff --git a/examples/mcp/agents/nextjs/index.ts b/examples/mcp/agents/nextjs/index.ts index 739ebab..8623dd1 100644 --- a/examples/mcp/agents/nextjs/index.ts +++ b/examples/mcp/agents/nextjs/index.ts @@ -26,6 +26,12 @@ interface AgentConfig { conversationHistory: Array<{ role: MessageRole; content: string }>; maxRetries: number; retryDelayMs: number; + iterationCount: number; + totalTokensUsed: number; + maxTokensPerRequest: number; + maxHistoryLength: number; + sessionId: string; + memoryEnabled: boolean; } class NextJSAgent { @@ -42,6 +48,12 @@ class NextJSAgent { conversationHistory: [], maxRetries: 3, retryDelayMs: 60000, + iterationCount: 0, + totalTokensUsed: 0, + maxTokensPerRequest: 1000, + maxHistoryLength: 10, + sessionId: `nextjs-agent-${Date.now()}`, + memoryEnabled: true, }; this.rl = readline.createInterface({ @@ -112,7 +124,21 @@ You have access to several MCP tool categories: **File System Tools:** -* Available for file operations in /tmp directory +* read_file: Read the contents of a file +* write_file: Write content to a file +* list_directory: List directory contents +* create_directory: Create directories +* delete_file: Delete files +* file_info: Get file information + +**NPM Tools:** + +* npm_run: Execute npm commands (install, build, start, test, etc.) +* npm_init: Initialize new npm project +* npm_install: Install npm packages +* create_nextjs_project: Create a new Next.js project with specified options + +**IMPORTANT**: All tools are called through the MCP system - never use XML-style syntax like . The LLM will automatically use the available tools when needed. --- @@ -146,6 +172,8 @@ When encountering HTTP errors or failures: **When creating a Next.js project, always wait 30 seconds after project creation.** +**CRITICAL: Never use XML-style tool syntax like \`\`. All tools are automatically available through MCP and will be called by the LLM when needed.** + 1. Clarify requirements and tech stack 2. Lookup technologies using Context7 tools 3. Retrieve current documentation and patterns @@ -154,6 +182,11 @@ When encountering HTTP errors or failures: 6. Include error handling, testing, and CI/build scripts 7. Prioritize maintainability, readability, and DX (developer experience) +**For Next.js projects:** +- Use \`create_nextjs_project\` tool to create new projects +- Use \`npm_run\` tool for npm commands like "run dev", "install", "build" +- Use filesystem tools to read/write files and list directories + --- ### ⚛️ NEXT.JS PROJECT RULES @@ -203,7 +236,6 @@ If a Next.js project exists: let attempt = 0; while (attempt < this.config.maxRetries) { try { - // Health check const isHealthy = await this.config.client.healthCheck(); if (!isHealthy) { console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); @@ -222,6 +254,22 @@ If a Next.js project exists: ].includes(tool.name) ); + const memoryTools = tools.data.filter((tool) => + ['save-state', 'restore-state', 'list-sessions'].includes(tool.name) + ); + + if (memoryTools.length > 0) { + console.info( + `🧠 Found ${memoryTools.length} memory management tools` + ); + await this.loadStateFromMemory(); + } else { + console.info( + '⚠️ No memory tools available. State persistence disabled.' + ); + this.config.memoryEnabled = false; + } + const context7Tools = [...realContext7Tools, ...mockContext7Tools]; if (context7Tools.length === 0) { @@ -347,8 +395,27 @@ If a Next.js project exists: case 'exit': case 'quit': console.log('\n👋 Thank you for using NextJS Agent! Goodbye!'); - this.rl.close(); - process.exit(0); + + if (this.config.memoryEnabled) { + console.log('💾 Saving session state before exit...'); + this.saveStateToMemoryForced('Manual exit via user command') + .then(() => { + console.log('✅ Session state saved successfully'); + this.rl.close(); + process.exit(0); + }) + .catch((error) => { + console.warn( + '⚠️ Failed to save session state:', + (error as Error).message + ); + this.rl.close(); + process.exit(0); + }); + } else { + this.rl.close(); + process.exit(0); + } return true; case 'clear': @@ -404,23 +471,41 @@ If a Next.js project exists: console.log(`\n🔍 Processing request: "${userInput}"`); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + this.config.iterationCount++; + const iterationStartTime = Date.now(); + + console.log(`🔄 Starting Iteration #${this.config.iterationCount}`); + console.log( + `📝 User Input: "${userInput.substring(0, 100)}${userInput.length > 100 ? '...' : ''}"` + ); + console.log(`⏰ Start Time: ${new Date().toLocaleTimeString()}`); + console.log('─'.repeat(60)); + this.config.conversationHistory.push({ role: MessageRole.user, content: userInput, }); + if (this.config.memoryEnabled) { + await this.saveStateToMemory( + `Before processing request: "${userInput.substring(0, 50)}..."` + ); + } + let assistantResponse = ''; let shouldWaitForProject = false; await this.config.client.streamChatCompletion( { model: `${this.config.provider}/${this.config.model}`, - messages: this.config.conversationHistory, - max_tokens: 2000, + messages: this.getOptimizedConversationHistory(), + max_tokens: this.config.maxTokensPerRequest, }, { onOpen: () => { - console.log('🔗 Starting development session with NextJS Agent...\n'); + console.log( + '\n🔗 Starting development session with NextJS Agent...\n' + ); }, onReasoning: (reasoning) => { console.log(`\n🤔 Agent Reasoning: ${reasoning}`); @@ -429,6 +514,42 @@ If a Next.js project exists: process.stdout.write(content); assistantResponse += content; }, + onUsageMetrics: (usage) => { + const iterationDuration = Date.now() - iterationStartTime; + this.config.totalTokensUsed += usage.total_tokens; + + console.log( + `\n\n💰 Iteration #${this.config.iterationCount} Token Usage:` + ); + console.log( + ` 📊 Prompt tokens: ${usage.prompt_tokens.toLocaleString()}` + ); + console.log( + ` ✍️ Completion tokens: ${usage.completion_tokens.toLocaleString()}` + ); + console.log( + ` 🎯 Total tokens: ${usage.total_tokens.toLocaleString()}` + ); + console.log(` ⏱️ Duration: ${iterationDuration}ms`); + console.log( + ` 🚀 Tokens/sec: ${Math.round((usage.total_tokens / iterationDuration) * 1000)}` + ); + + console.log(`\n📈 Cumulative Session Usage:`); + console.log(` 🔢 Total Iterations: ${this.config.iterationCount}`); + console.log( + ` 🎯 Total Tokens Used: ${this.config.totalTokensUsed.toLocaleString()}` + ); + console.log( + ` 📈 Average Tokens per Iteration: ${Math.round(this.config.totalTokensUsed / this.config.iterationCount).toLocaleString()}` + ); + + const estimatedCost = this.config.totalTokensUsed * 0.000001; + console.log( + ` 💰 Estimated Total Cost: $${estimatedCost.toFixed(6)}` + ); + console.log('─'.repeat(60)); + }, onMCPTool: (toolCall) => { console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); try { @@ -453,6 +574,14 @@ If a Next.js project exists: }, onError: (error) => { console.error(`\n❌ Stream Error: ${error.error}`); + + // Save error state to memory for recovery + if (this.config.memoryEnabled) { + this.saveStateToMemory( + `Error occurred during request processing: ${error.error}` + ).catch(console.warn); + } + throw new Error(`Stream error: ${error.error}`); }, onFinish: async () => { @@ -463,7 +592,6 @@ If a Next.js project exists: await this.waitForProjectCreation(); } - // Add assistant response to conversation history if (assistantResponse.trim()) { this.config.conversationHistory.push({ role: MessageRole.assistant, @@ -475,7 +603,377 @@ If a Next.js project exists: ); } + /** + * Save current state to memory MCP server + */ + private async saveStateToMemory(context: string): Promise { + if (!this.config.memoryEnabled) return; + + try { + const state = { + conversationHistory: this.config.conversationHistory.slice(-5), + iterationCount: this.config.iterationCount, + totalTokensUsed: this.config.totalTokensUsed, + timestamp: new Date().toISOString(), + }; + + console.log( + `💾 Saving state to memory for session: ${this.config.sessionId}` + ); + + let toolCallDetected = false; + let saveSuccessful = false; + + await this.config.client.streamChatCompletion( + { + model: `${this.config.provider}/${this.config.model}`, + messages: [ + { + role: MessageRole.system, + content: `You are a memory manager. You MUST call the save-state tool now with the provided data. Don't explain - just call the tool immediately. + +SessionID: ${this.config.sessionId} +State: ${JSON.stringify(state)} +Context: ${context} + +Call save-state tool immediately with sessionId="${this.config.sessionId}" and the state object above.`, + }, + { + role: MessageRole.user, + content: `Call save-state tool now with sessionId="${this.config.sessionId}"`, + }, + ], + max_tokens: 50, + }, + { + onMCPTool: (toolCall) => { + toolCallDetected = true; + console.log(`📱 Memory tool called: ${toolCall.function.name}`); + + if ( + toolCall.function.name === 'save-state' || + toolCall.function.name === 'save-error-state' + ) { + saveSuccessful = true; + console.log('✅ State save tool invoked successfully'); + } + }, + onContent: () => { + // Suppress content output for memory saves + }, + onError: (error) => { + console.warn('⚠️ Memory save failed:', error.error); + }, + onFinish: () => { + if (toolCallDetected && saveSuccessful) { + console.log('✅ Memory save completed successfully'); + } else if (!toolCallDetected) { + console.warn( + '⚠️ No memory tool was called - memory may not be available' + ); + } else { + console.warn('⚠️ Memory tool called but save may have failed'); + } + }, + } + ); + } catch (error) { + console.warn( + '⚠️ Failed to save state to memory:', + (error as Error).message + ); + } + } + + /** + * Save current state to memory MCP server with forced tool usage + */ + private async saveStateToMemoryForced(context: string): Promise { + if (!this.config.memoryEnabled) return; + + try { + const state = { + conversationHistory: this.config.conversationHistory.slice(-5), + iterationCount: this.config.iterationCount, + totalTokensUsed: this.config.totalTokensUsed, + timestamp: new Date().toISOString(), + }; + + console.log( + `💾 Forcing memory save for session: ${this.config.sessionId}` + ); + + let toolCallDetected = false; + let saveSuccessful = false; + const maxAttempts = 3; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + console.log(`🔄 Memory save attempt ${attempt}/${maxAttempts}`); + + try { + await this.config.client.streamChatCompletion( + { + model: `${this.config.provider}/${this.config.model}`, + messages: [ + { + role: MessageRole.system, + content: `You are a memory manager. You MUST call the save-state tool immediately. No explanations, no acknowledgments - just call the tool. + +CRITICAL: You MUST call save-state tool with these exact parameters: +- sessionId: "${this.config.sessionId}" +- state: ${JSON.stringify(state)} +- context: "${context}" + +Call the save-state tool now.`, + }, + { + role: MessageRole.user, + content: `Call save-state tool immediately with sessionId="${this.config.sessionId}". Do not respond with text - only call the tool.`, + }, + ], + max_tokens: 100, + }, + { + onMCPTool: (toolCall) => { + toolCallDetected = true; + console.log(`📱 Memory tool called: ${toolCall.function.name}`); + + if ( + toolCall.function.name === 'save-state' || + toolCall.function.name === 'save-error-state' + ) { + saveSuccessful = true; + console.log('✅ Memory tool invoked successfully'); + try { + const args = JSON.parse(toolCall.function.arguments); + console.log( + `📝 Tool arguments:`, + JSON.stringify(args, null, 2) + ); + } catch { + // Ignore parsing errors + } + } + }, + onContent: () => { + // Suppress content output for memory saves + }, + onError: (error) => { + console.warn( + `⚠️ Memory save attempt ${attempt} failed:`, + error.error + ); + }, + onFinish: () => { + if (toolCallDetected && saveSuccessful) { + console.log( + `✅ Memory save completed successfully on attempt ${attempt}` + ); + } else if (!toolCallDetected) { + console.warn( + `⚠️ Attempt ${attempt}: No memory tool was called` + ); + } else { + console.warn( + `⚠️ Attempt ${attempt}: Memory tool called but save may have failed` + ); + } + }, + } + ); + + if (toolCallDetected && saveSuccessful) { + break; + } + + if (attempt < maxAttempts) { + console.log(`⏳ Waiting 2 seconds before retry...`); + await this.delay(2000); + } + } catch (attemptError) { + console.warn( + `⚠️ Memory save attempt ${attempt} error:`, + (attemptError as Error).message + ); + if (attempt === maxAttempts) { + throw attemptError; + } + } + } + + if (!toolCallDetected || !saveSuccessful) { + console.error( + `❌ Failed to save memory after ${maxAttempts} attempts - memory tools may not be available` + ); + } + } catch (error) { + console.warn( + '⚠️ Failed to save state to memory:', + (error as Error).message + ); + } + } + + /** + * Load state from memory MCP server (via chat completion) + */ + private async loadStateFromMemory(): Promise { + if (!this.config.memoryEnabled) return false; + + try { + console.log( + `📥 Attempting to restore state for session: ${this.config.sessionId}` + ); + + let restoredData: any = null; + + await this.config.client.streamChatCompletion( + { + model: `${this.config.provider}/${this.config.model}`, + messages: [ + { + role: MessageRole.system, + content: `You have access to memory management tools. Restore the saved state for session "${this.config.sessionId}".`, + }, + { + role: MessageRole.user, + content: `Please restore the session state using the restore-state tool and provide the restored data.`, + }, + ], + max_tokens: 200, + }, + { + onContent: (content) => { + if (content.includes('{') && content.includes('}')) { + try { + const jsonMatch = content.match(/\{.*\}/s); + if (jsonMatch) { + restoredData = JSON.parse(jsonMatch[0]); + } + } catch { + // Ignore parsing errors + } + } + }, + onMCPTool: (toolCall) => { + console.log(`📱 Memory tool called: ${toolCall.function.name}`); + }, + onError: () => { + console.log('ℹ️ No previous state found'); + }, + onFinish: () => { + if (restoredData && restoredData.state) { + this.config.conversationHistory = + restoredData.state.conversationHistory || []; + this.config.iterationCount = + restoredData.state.iterationCount || 0; + this.config.totalTokensUsed = + restoredData.state.totalTokensUsed || 0; + + console.log( + `✅ Restored state from ${restoredData.state.timestamp}` + ); + console.log( + `📊 Restored ${this.config.conversationHistory.length} messages` + ); + console.log( + `🔢 Restored iteration count: ${this.config.iterationCount}` + ); + } + }, + } + ); + + return !!restoredData; + } catch (error) { + console.log(`ℹ️ No previous state found: ${(error as Error).message}`); + return false; + } + } + + /** + * Truncate conversation history to stay within token limits + */ + private truncateConversationHistory(): void { + if ( + this.config.conversationHistory.length <= + this.config.maxHistoryLength + 1 + ) { + return; + } + + console.log( + `✂️ Truncating conversation history from ${this.config.conversationHistory.length} to ${this.config.maxHistoryLength + 1} messages` + ); + + const systemPrompt = this.config.conversationHistory[0]; + + const recentMessages = this.config.conversationHistory.slice( + -this.config.maxHistoryLength + ); + + const truncatedMessages = this.config.conversationHistory.slice( + 1, + -this.config.maxHistoryLength + ); + + if (truncatedMessages.length > 0) { + this.saveStateToMemoryForced( + `Truncated ${truncatedMessages.length} older messages` + ).catch((error) => { + console.warn( + '⚠️ Failed to save truncated messages to memory:', + (error as Error).message + ); + }); + } + + this.config.conversationHistory = [systemPrompt, ...recentMessages]; + } + + /** + * Estimate token count for a message (rough approximation) + */ + private estimateTokenCount(text: string): number { + // Rough approximation: 1 token ≈ 4 characters + return Math.ceil(text.length / 4); + } + + /** + * Get optimized conversation history for the current request + */ + private getOptimizedConversationHistory(): Array<{ + role: MessageRole; + content: string; + }> { + this.truncateConversationHistory(); + + const totalEstimatedTokens = this.config.conversationHistory.reduce( + (sum, msg) => sum + this.estimateTokenCount(msg.content), + 0 + ); + + console.log(`📊 Estimated tokens in conversation: ${totalEstimatedTokens}`); + + return this.config.conversationHistory; + } + async shutdown(): Promise { + if (this.config.memoryEnabled) { + console.log('💾 Saving session state before shutdown...'); + try { + await this.saveStateToMemoryForced( + 'Manual shutdown via SIGINT/SIGTERM signal' + ); + console.log('✅ Session state saved successfully'); + } catch (error) { + console.warn( + '⚠️ Failed to save session state:', + (error as Error).message + ); + } + } + this.rl.close(); } } diff --git a/examples/mcp/agents/vite/index.ts b/examples/mcp/agents/vite/index.ts index 7fc2166..a5060d3 100644 --- a/examples/mcp/agents/vite/index.ts +++ b/examples/mcp/agents/vite/index.ts @@ -26,6 +26,8 @@ interface AgentConfig { conversationHistory: Array<{ role: MessageRole; content: string }>; maxRetries: number; retryDelayMs: number; + iterationCount: number; + totalTokensUsed: number; } class ViteAgent { @@ -42,6 +44,8 @@ class ViteAgent { conversationHistory: [], maxRetries: 3, retryDelayMs: 60000, + iterationCount: 0, + totalTokensUsed: 0, }; this.rl = readline.createInterface({ @@ -428,6 +432,17 @@ If a Vite project exists: console.log(`\n🔍 Processing Vite request: "${userInput}"`); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + // Increment iteration count and start timing + this.config.iterationCount++; + const iterationStartTime = Date.now(); + + console.log(`🔄 Starting Iteration #${this.config.iterationCount}`); + console.log( + `📝 User Input: "${userInput.substring(0, 100)}${userInput.length > 100 ? '...' : ''}"` + ); + console.log(`⏰ Start Time: ${new Date().toLocaleTimeString()}`); + console.log('─'.repeat(60)); + this.config.conversationHistory.push({ role: MessageRole.user, content: userInput, @@ -455,6 +470,43 @@ If a Vite project exists: process.stdout.write(content); assistantResponse += content; }, + onUsageMetrics: (usage) => { + const iterationDuration = Date.now() - iterationStartTime; + this.config.totalTokensUsed += usage.total_tokens; + + console.log( + `\n\n💰 Iteration #${this.config.iterationCount} Token Usage:` + ); + console.log( + ` 📊 Prompt tokens: ${usage.prompt_tokens.toLocaleString()}` + ); + console.log( + ` ✍️ Completion tokens: ${usage.completion_tokens.toLocaleString()}` + ); + console.log( + ` 🎯 Total tokens: ${usage.total_tokens.toLocaleString()}` + ); + console.log(` ⏱️ Duration: ${iterationDuration}ms`); + console.log( + ` 🚀 Tokens/sec: ${Math.round((usage.total_tokens / iterationDuration) * 1000)}` + ); + + console.log(`\n📈 Cumulative Session Usage:`); + console.log(` 🔢 Total Iterations: ${this.config.iterationCount}`); + console.log( + ` 🎯 Total Tokens Used: ${this.config.totalTokensUsed.toLocaleString()}` + ); + console.log( + ` 📈 Average Tokens per Iteration: ${Math.round(this.config.totalTokensUsed / this.config.iterationCount).toLocaleString()}` + ); + + // Calculate estimated cost (example rates - adjust based on provider) + const estimatedCost = this.config.totalTokensUsed * 0.000001; + console.log( + ` 💰 Estimated Total Cost: $${estimatedCost.toFixed(6)}` + ); + console.log('─'.repeat(60)); + }, onMCPTool: (toolCall: any) => { console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); try { diff --git a/examples/mcp/docker-compose.yml b/examples/mcp/docker-compose.yml index f463130..74b4fb7 100644 --- a/examples/mcp/docker-compose.yml +++ b/examples/mcp/docker-compose.yml @@ -5,7 +5,7 @@ services: - '8080:8080' environment: # General settings - ENVIRONMENT: development + ENVIRONMENT: production # Enable MCP support MCP_ENABLE: 'true' @@ -97,7 +97,7 @@ services: RUN npm install COPY . . EXPOSE 3001 - CMD ["node", "index-http.js"] + CMD ["node", "index.js"] environment: NODE_ENV: 'production' MCP_SERVER_NAME: 'web-search' diff --git a/examples/mcp/mcp-servers/context7/index.js b/examples/mcp/mcp-servers/context7/index.js index d7b6204..5c3d840 100644 --- a/examples/mcp/mcp-servers/context7/index.js +++ b/examples/mcp/mcp-servers/context7/index.js @@ -10,11 +10,20 @@ import express from 'express'; import cors from 'cors'; import { spawn } from 'node:child_process'; import { randomUUID } from 'node:crypto'; +import { + createMcpLogger, + logMcpRequest, + logMcpSession, + logMcpToolCall, + logMcpError, +} from './logger.js'; const app = express(); app.use(express.json()); app.use(cors()); +const logger = createMcpLogger('mcp-context7', '1.0.0'); + const mcpSessions = new Map(); const context7Processes = new Map(); @@ -36,7 +45,9 @@ class Context7Process { } this.readyPromise = new Promise((resolve, reject) => { - console.info('🚀 Spawning Context7 MCP server...'); + logger.info('Spawning Context7 MCP server', { + service: 'context7-process', + }); this.process = spawn('npx', ['-y', '@upstash/context7-mcp@latest'], { stdio: ['pipe', 'pipe', 'pipe'], @@ -59,7 +70,10 @@ class Context7Process { const message = JSON.parse(line.trim()); this.handleMessage(message); } catch { - console.warn('📝 Non-JSON output from Context7:', line.trim()); + logger.warn('Non-JSON output from Context7', { + service: 'context7-process', + output: line.trim(), + }); } } } @@ -69,16 +83,24 @@ class Context7Process { this.process.stderr.on('data', (data) => { const message = data.toString().trim(); if (message.includes('ready') || message.includes('listening')) { - console.info('✅ Context7 MCP server ready'); + logger.info('Context7 MCP server ready', { + service: 'context7-process', + }); this.isReady = true; resolve(); } else { - console.info('📝 Context7 log:', message); + logger.info('Context7 log', { + service: 'context7-process', + message, + }); } }); this.process.on('exit', (code) => { - console.info(`🔚 Context7 process exited with code ${code}`); + logger.info('Context7 process exited', { + service: 'context7-process', + exitCode: code, + }); this.isReady = false; this.process = null; @@ -89,7 +111,9 @@ class Context7Process { }); this.process.on('error', (error) => { - console.error('❌ Context7 process error:', error); + logMcpError(logger, 'Context7 process error', error, { + service: 'context7-process', + }); reject(error); }); globalThis.setTimeout(() => { @@ -101,7 +125,9 @@ class Context7Process { } sendInitialize() { - console.info('🔧 Sending initialize to Context7...'); + logger.info('Sending initialize to Context7', { + service: 'context7-process', + }); const initMessage = { jsonrpc: '2.0', id: this.nextMessageId(), @@ -134,7 +160,9 @@ class Context7Process { initPromise .then(() => { - console.info('✅ Context7 initialized successfully'); + logger.info('Context7 initialized successfully', { + service: 'context7-process', + }); this.isReady = true; globalThis.setTimeout(() => { this.sendMessage({ @@ -144,7 +172,10 @@ class Context7Process { }, 100); }) .catch((error) => { - console.error('❌ Context7 initialization failed:', error); + logMcpError(logger, error, { + service: 'context7-process', + operation: 'initialization', + }); }); } @@ -158,27 +189,40 @@ class Context7Process { } const jsonMessage = JSON.stringify(message) + '\n'; - console.info('📤 Sending to Context7:', JSON.stringify(message, null, 2)); + logger.debug('Sending message to Context7', { + service: 'context7-process', + method: message.method, + id: message.id, + message: jsonMessage.trim(), + }); this.process.stdin.write(jsonMessage); } handleMessage(message) { - console.info( - '📥 Received from Context7:', - JSON.stringify(message, null, 2) - ); + logger.debug('Received message from Context7', { + service: 'context7-process', + method: message.method, + id: message.id, + hasError: !!message.error, + }); if (message.id && this.pendingRequests.has(message.id)) { const { resolve, reject } = this.pendingRequests.get(message.id); this.pendingRequests.delete(message.id); if (message.error) { - console.error('❌ Context7 error response:', message.error); + logMcpError(logger, message.error, { + service: 'context7-process', + requestId: message.id, + }); reject(new Error(message.error.message || 'Context7 error')); return; } - console.info('✅ Context7 success response for ID', message.id); + logger.debug('Context7 success response', { + service: 'context7-process', + requestId: message.id, + }); resolve(message.result || message); return; } @@ -188,28 +232,36 @@ class Context7Process { message.result.serverInfo && message.result.serverInfo.name === 'Context7' ) { - console.info( - '✅ Context7 initialized successfully (detected from serverInfo)' - ); + logger.info('Context7 initialized successfully from serverInfo', { + service: 'context7-process', + serverInfo: message.result.serverInfo, + }); this.isReady = true; return; } switch (message.method) { case 'notifications/initialized': - console.info('✅ Context7 initialized notification received'); + logger.info('Context7 initialized notification received', { + service: 'context7-process', + }); this.isReady = true; break; default: if (message.method) { - console.info('📢 Context7 notification/method:', message.method); + logger.debug('Context7 notification received', { + service: 'context7-process', + method: message.method, + }); } else if (message.id) { - console.warn( - '⚠️ Received response for unknown request ID:', - message.id - ); + logger.warn('Received response for unknown request ID', { + service: 'context7-process', + requestId: message.id, + }); } else { - console.info('ℹ️ Context7 message (no ID or method)'); + logger.debug('Context7 message with no ID or method', { + service: 'context7-process', + }); } } } @@ -245,7 +297,9 @@ class Context7Process { } requestToolsList() { - console.info('🔍 Requesting tools list from Context7...'); + logger.info('Requesting tools list from Context7', { + service: 'context7-process', + }); const toolsListMessage = { jsonrpc: '2.0', id: this.nextMessageId(), @@ -269,13 +323,16 @@ class Context7Process { toolsPromise .then((result) => { - console.info( - '✅ Context7 tools list received:', - JSON.stringify(result, null, 2) - ); + logger.info('Context7 tools list received', { + service: 'context7-process', + toolsCount: result?.tools?.length || 0, + }); }) .catch((error) => { - console.error('❌ Context7 tools list failed:', error); + logMcpError(logger, error, { + service: 'context7-process', + operation: 'tools-list', + }); }); } @@ -307,12 +364,16 @@ class Context7Process { terminate() { if (this.process) { - console.info('🔄 Terminating Context7 process...'); + logger.info('Terminating Context7 process', { + service: 'context7-process', + }); this.process.kill('SIGTERM'); globalThis.setTimeout(() => { if (this.process) { - console.info('🔪 Force killing Context7 process...'); + logger.warn('Force killing Context7 process', { + service: 'context7-process', + }); this.process.kill('SIGKILL'); } }, 5000); @@ -353,10 +414,10 @@ async function handleMcpRequest(request) { switch (request.method) { case 'initialize': { - console.info( - '🔧 MCP initialize request:', - JSON.stringify(request, null, 2) - ); + logger.info('MCP initialize request received', { + protocolVersion: request.params?.protocolVersion, + clientInfo: request.params?.clientInfo, + }); return { jsonrpc: '2.0', @@ -375,7 +436,7 @@ async function handleMcpRequest(request) { } case 'tools/list': { - console.info('📋 MCP tools/list request'); + logger.info('MCP tools/list request received'); return { jsonrpc: '2.0', @@ -430,9 +491,11 @@ async function handleMcpRequest(request) { } case 'tools/call': { - console.info( - '🔧 MCP tools/call request:', - JSON.stringify(request, null, 2) + logMcpToolCall( + logger, + request.params?.name, + 'unknown', + request.params?.arguments || {} ); if (!request?.params?.name) { @@ -443,47 +506,26 @@ async function handleMcpRequest(request) { switch (name) { case 'c41_resolve-library-id': { - console.info(`🔍 Resolving library ID for: ${args.libraryName}`); - console.info(`📝 Input arguments:`, JSON.stringify(args, null, 2)); + logger.info('Resolving library ID', { + libraryName: args.libraryName, + }); try { const context7 = getContext7Process(); - console.info(`🚀 Calling Context7 resolve-library-id tool...`); + logger.debug('Calling Context7 resolve-library-id tool'); const result = await context7.callTool('resolve-library-id', { libraryName: args.libraryName, }); - console.info( - `✅ Context7 resolve-library-id raw result:`, - JSON.stringify(result, null, 2) - ); - console.info(`📊 Result structure analysis:`); - console.info(` - Result type: ${typeof result}`); - console.info( - ` - Has content array: ${Array.isArray(result.content)}` - ); - console.info(` - Content length: ${result.content?.length || 0}`); - - if (result.content?.[0]) { - console.info( - ` - First content item type: ${result.content[0].type}` - ); - console.info( - ` - First content text length: ${result.content[0].text?.length || 0}` - ); - console.info( - ` - First content text preview: ${result.content[0].text?.substring(0, 200)}...` - ); - } + logger.info('Context7 resolve-library-id completed', { + libraryName: args.libraryName, + hasContent: !!result.content, + contentLength: result.content?.[0]?.text?.length || 0, + }); const responseText = result.content?.[0]?.text || JSON.stringify(result, null, 2); - console.info( - `📤 Sending response text (${responseText.length} chars):`, - responseText.substring(0, 500) + - (responseText.length > 500 ? '...' : '') - ); return { jsonrpc: '2.0', @@ -498,16 +540,12 @@ async function handleMcpRequest(request) { }, }; } catch (error) { - console.error('❌ Error resolving library ID:', error); - console.error('❌ Error stack:', error.stack); - console.error('❌ Error details:', { - name: error.name, - message: error.message, - cause: error.cause, + logMcpError(logger, error, { + libraryName: args.libraryName, + tool: 'resolve-library-id', }); const errorText = `Error resolving library ID for "${args.libraryName}": ${error.message}`; - console.info(`📤 Sending error response:`, errorText); return { jsonrpc: '2.0', @@ -525,14 +563,15 @@ async function handleMcpRequest(request) { } case 'c41_get-library-docs': { - console.info( - `📚 Getting documentation for: ${args.context7CompatibleLibraryID}` - ); - console.info(`📝 Input arguments:`, JSON.stringify(args, null, 2)); + logger.info('Getting library documentation', { + context7CompatibleLibraryID: args.context7CompatibleLibraryID, + tokens: args.tokens || 10000, + topic: args.topic, + }); try { const context7 = getContext7Process(); - console.info(`🚀 Calling Context7 get-library-docs tool...`); + logger.debug('Calling Context7 get-library-docs tool'); const callArgs = { context7CompatibleLibraryID: args.context7CompatibleLibraryID, @@ -543,46 +582,19 @@ async function handleMcpRequest(request) { callArgs.topic = args.topic; } - console.info( - `📝 Context7 call arguments:`, - JSON.stringify(callArgs, null, 2) - ); - const result = await context7.callTool( 'get-library-docs', callArgs ); - console.info( - `✅ Context7 get-library-docs raw result:`, - JSON.stringify(result, null, 2) - ); - console.info(`📊 Result structure analysis:`); - console.info(` - Result type: ${typeof result}`); - console.info( - ` - Has content array: ${Array.isArray(result.content)}` - ); - console.info(` - Content length: ${result.content?.length || 0}`); - - if (result.content?.[0]) { - console.info( - ` - First content item type: ${result.content[0].type}` - ); - console.info( - ` - First content text length: ${result.content[0].text?.length || 0}` - ); - console.info( - ` - First content text preview: ${result.content[0].text?.substring(0, 200)}...` - ); - } + logger.info('Context7 get-library-docs completed', { + context7CompatibleLibraryID: args.context7CompatibleLibraryID, + hasContent: !!result.content, + contentLength: result.content?.[0]?.text?.length || 0, + }); const responseText = result.content?.[0]?.text || JSON.stringify(result, null, 2); - console.info( - `📤 Sending response text (${responseText.length} chars):`, - responseText.substring(0, 500) + - (responseText.length > 500 ? '...' : '') - ); return { jsonrpc: '2.0', @@ -597,16 +609,12 @@ async function handleMcpRequest(request) { }, }; } catch (error) { - console.error('❌ Error getting library documentation:', error); - console.error('❌ Error stack:', error.stack); - console.error('❌ Error details:', { - name: error.name, - message: error.message, - cause: error.cause, + logMcpError(logger, error, { + context7CompatibleLibraryID: args.context7CompatibleLibraryID, + tool: 'get-library-docs', }); const errorText = `Error getting documentation for "${args.context7CompatibleLibraryID}": ${error.message}`; - console.info(`📤 Sending error response:`, errorText); return { jsonrpc: '2.0', @@ -639,9 +647,7 @@ async function handleMcpRequest(request) { function setupSessionRoutes() { app.post('/mcp', async (req, res) => { try { - console.info('📨 MCP POST request received:'); - console.info(' Headers: %s', JSON.stringify(req.headers, null, 2)); - console.info(' Body: %s', JSON.stringify(req.body, null, 2)); + logMcpRequest(logger, req); if (!req.body || typeof req.body !== 'object') { return res.status(400).json({ @@ -671,13 +677,13 @@ function setupSessionRoutes() { mcpSessions.set(sessionId, { createdAt: Date.now(), }); - console.info(`🎯 MCP session created: ${sessionId}`); + logMcpSession(logger, 'created', sessionId); globalThis.setTimeout( () => { if (mcpSessions.has(sessionId)) { mcpSessions.delete(sessionId); - console.info(`🧹 Cleaned up session: ${sessionId}`); + logMcpSession(logger, 'cleaned up', sessionId); } }, 10 * 60 * 1000 @@ -689,7 +695,10 @@ function setupSessionRoutes() { res.setHeader('mcp-session-id', sessionId); res.json(response); } catch (error) { - console.error('❌ Error handling MCP request:', error); + logMcpError(logger, error, { + endpoint: '/mcp', + method: 'POST', + }); if (!res.headersSent) { res.status(500).json({ @@ -752,7 +761,7 @@ function setupSessionRoutes() { mcpSessions.delete(sessionId); - console.info(`🗑️ Session terminated: ${sessionId}`); + logMcpSession(logger, 'terminated', sessionId); res.status(200).json({ jsonrpc: '2.0', result: { status: 'terminated' }, @@ -770,7 +779,7 @@ app.get('/health', (_req, res) => { timestamp: new Date().toISOString(), }; - console.info('💚 Health check requested: %j', healthStatus); + logger.info('Health check requested', healthStatus); res.json(healthStatus); }); @@ -784,31 +793,27 @@ async function startServer() { setupSessionRoutes(); app.listen(PORT, host, () => { - console.info(`🌉 Context7 HTTP Bridge running on http://${host}:${PORT}`); - console.info('📋 Protocol: Model Context Protocol (MCP) HTTP Bridge'); - console.info('🎯 Target: Context7 MCP Server (stdio)'); - console.info('🔗 Available endpoints:'); - console.info(' POST /mcp - MCP protocol endpoint'); - console.info( - ' GET /mcp - SSE notifications (with session-id header)' - ); - console.info( - ' DELETE /mcp - Session termination (with session-id header)' - ); - console.info(' GET /health - Health check'); - console.info('🛠️ Available tools:'); - console.info( - ' - c41_resolve-library-id - Resolve library names to Context7 IDs' - ); - console.info( - ' - c41_get-library-docs - Fetch up-to-date library documentation' - ); - console.info('🚀 Bridge ready for connections'); + logger.info('Context7 HTTP Bridge started', { + host, + port: PORT, + protocol: 'Model Context Protocol (MCP) HTTP Bridge', + target: 'Context7 MCP Server (stdio)', + endpoints: { + mcp: 'POST /mcp - MCP protocol endpoint', + mcpSse: 'GET /mcp - SSE notifications (with session-id header)', + mcpDelete: 'DELETE /mcp - Session termination (with session-id header)', + health: 'GET /health - Health check', + }, + tools: [ + 'c41_resolve-library-id - Resolve library names to Context7 IDs', + 'c41_get-library-docs - Fetch up-to-date library documentation', + ], + }); }); } process.on('SIGTERM', () => { - console.info('🔄 Received SIGTERM, shutting down gracefully'); + logger.info('Received SIGTERM, shutting down gracefully'); mcpSessions.clear(); @@ -820,7 +825,7 @@ process.on('SIGTERM', () => { }); process.on('SIGINT', () => { - console.info('🔄 Received SIGINT, shutting down gracefully'); + logger.info('Received SIGINT, shutting down gracefully'); mcpSessions.clear(); @@ -832,6 +837,8 @@ process.on('SIGINT', () => { }); startServer().catch((error) => { - console.error('💥 Failed to start server:', error); + logMcpError(logger, error, { + operation: 'server-startup', + }); process.exit(1); }); diff --git a/examples/mcp/mcp-servers/context7/logger.js b/examples/mcp/mcp-servers/context7/logger.js new file mode 100644 index 0000000..db86d11 --- /dev/null +++ b/examples/mcp/mcp-servers/context7/logger.js @@ -0,0 +1,97 @@ +/** + * Standardized Winston Logger for MCP Servers + * + * Provides consistent, single-line logging across all MCP servers + * with structured metadata and unified formatting. + */ + +import winston from 'winston'; + +/** + * Create a standardized logger for MCP servers + * @param {string} serviceName - The name of the MCP service + * @param {string} [version='1.0.0'] - The version of the service + * @param {string} [logLevel] - Override log level (defaults to LOG_LEVEL env var or 'info') + * @returns {winston.Logger} Configured Winston logger + */ +export function createMcpLogger(serviceName, version = '1.0.0', logLevel) { + const level = logLevel || process.env.LOG_LEVEL || 'info'; + + return winston.createLogger({ + level, + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }), + winston.format.errors({ stack: true }), + winston.format.json() + ), + defaultMeta: { + service: serviceName, + version, + protocol: 'Model Context Protocol', + transport: 'Streamable HTTP', + }, + transports: [ + new winston.transports.Console({ + handleExceptions: true, + handleRejections: true, + }), + ], + }); +} + +/** + * Log MCP request received + */ +export function logMcpRequest(logger, req, additionalMeta = {}) { + const sessionId = req.headers['mcp-session-id']; + const method = req.body?.method; + const id = req.body?.id; + + logger.info('MCP request received', { + sessionId, + method, + requestId: id, + userAgent: req.headers['user-agent'], + contentLength: req.headers['content-length'], + ...additionalMeta, + }); +} + +/** + * Log MCP session events + */ +export function logMcpSession(logger, event, sessionId, additionalMeta = {}) { + logger.info(`MCP session ${event}`, { + sessionId, + ...additionalMeta, + }); +} + +/** + * Log MCP tool calls + */ +export function logMcpToolCall( + logger, + toolName, + sessionId, + args = {}, + additionalMeta = {} +) { + logger.info(`MCP tool called: ${toolName}`, { + sessionId, + tool: toolName, + args: Object.keys(args), + ...additionalMeta, + }); +} + +/** + * Log MCP errors with context + */ +export function logMcpError(logger, error, context = {}) { + logger.error('MCP error occurred', { + error: error.message, + stack: error.stack, + ...context, + }); +} diff --git a/examples/mcp/mcp-servers/context7/package-lock.json b/examples/mcp/mcp-servers/context7/package-lock.json index 4f52616..caeafb4 100644 --- a/examples/mcp/mcp-servers/context7/package-lock.json +++ b/examples/mcp/mcp-servers/context7/package-lock.json @@ -11,14 +11,34 @@ "@modelcontextprotocol/sdk": "^0.5.0", "@upstash/context7-mcp": "latest", "cors": "^2.8.5", - "dotenv": "^16.3.1", "express": "^4.18.2", + "winston": "^3.17.0", "zod": "^3.22.4" }, "devDependencies": { "nodemon": "^3.0.1" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.5.0.tgz", @@ -30,6 +50,12 @@ "zod": "^3.23.8" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@upstash/context7-mcp": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@upstash/context7-mcp/-/context7-mcp-1.0.12.tgz", @@ -389,6 +415,12 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -535,6 +567,51 @@ "fsevents": "~2.3.2" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -665,6 +742,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -813,6 +896,12 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -844,6 +933,12 @@ "node": ">= 0.8" } }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1032,6 +1127,12 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1084,6 +1185,18 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1096,6 +1209,35 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1299,6 +1441,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1425,6 +1576,20 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1506,6 +1671,15 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1678,6 +1852,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1691,6 +1874,15 @@ "node": ">=10" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1700,6 +1892,15 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1713,6 +1914,12 @@ "node": ">=4" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1745,6 +1952,15 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1783,6 +1999,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1816,6 +2038,42 @@ "node": ">= 8" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/examples/mcp/mcp-servers/context7/package.json b/examples/mcp/mcp-servers/context7/package.json index 6c487dc..0815dbf 100644 --- a/examples/mcp/mcp-servers/context7/package.json +++ b/examples/mcp/mcp-servers/context7/package.json @@ -13,6 +13,7 @@ "cors": "^2.8.5", "@upstash/context7-mcp": "latest", "@modelcontextprotocol/sdk": "^0.5.0", + "winston": "^3.17.0", "zod": "^3.22.4" }, "devDependencies": { diff --git a/examples/mcp/mcp-servers/filesystem/index.js b/examples/mcp/mcp-servers/filesystem/index.js index 844cc12..641c466 100644 --- a/examples/mcp/mcp-servers/filesystem/index.js +++ b/examples/mcp/mcp-servers/filesystem/index.js @@ -13,11 +13,21 @@ import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import { promises as fs } from 'node:fs'; import path from 'node:path'; +import { + createMcpLogger, + logMcpRequest, + logMcpSession, + logMcpToolCall, + logMcpError, +} from './logger.js'; // Express app for HTTP transport const app = express(); app.use(express.json()); +// Create standardized logger +const logger = createMcpLogger('mcp-filesystem', '1.0.0'); + // Map to store transports by session ID const transports = {}; @@ -26,7 +36,9 @@ const allowedDirectories = (process.env.ALLOWED_DIRECTORIES || '/tmp').split( ',' ); -console.info('Allowed directories:', allowedDirectories); +logger.info('Filesystem server starting', { + allowedDirectories, +}); /** * Check if a path is within allowed directories @@ -61,7 +73,7 @@ function createMcpServer() { } try { - console.info(`Reading file: ${filePath}`); + logMcpToolCall(logger, 'read_file', { filePath }); const content = await fs.readFile(filePath, 'utf8'); return { @@ -73,7 +85,7 @@ function createMcpServer() { ], }; } catch (error) { - console.error(`Failed to read file ${filePath}:`, error.message); + logMcpError(logger, error, { filePath, operation: 'read_file' }); let errorMessage = `Failed to read file: ${filePath}\n`; if (error.code === 'ENOENT') { @@ -112,7 +124,10 @@ function createMcpServer() { } try { - console.info(`Writing to file: ${filePath}`); + logMcpToolCall(logger, 'write_file', { + filePath, + contentLength: content.length, + }); const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); @@ -128,7 +143,7 @@ function createMcpServer() { ], }; } catch (error) { - console.error(`Failed to write file ${filePath}:`, error.message); + logMcpError(logger, error, { filePath, operation: 'write_file' }); return { content: [ @@ -155,7 +170,7 @@ function createMcpServer() { } try { - console.info(`Listing directory: ${dirPath}`); + logMcpToolCall(logger, 'list_directory', { dirPath }); const entries = await fs.readdir(dirPath, { withFileTypes: true }); @@ -171,10 +186,11 @@ function createMcpServer() { modified: stats.mtime.toISOString(), }; } catch (error) { - console.warn( - `Could not get stats for ${entry.name}:`, - error.message - ); + logger.warn('Could not get stats for entry', { + entryName: entry.name, + dirPath, + error: error.message, + }); return { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file', @@ -203,7 +219,7 @@ function createMcpServer() { ], }; } catch (error) { - console.error(`Failed to list directory ${dirPath}:`, error.message); + logMcpError(logger, error, { dirPath, operation: 'list_directory' }); let errorMessage = `Failed to list directory: ${dirPath}\n`; if (error.code === 'ENOENT') { @@ -241,7 +257,7 @@ function createMcpServer() { } try { - console.info(`Creating directory: ${dirPath}`); + logMcpToolCall(logger, 'create_directory', { dirPath }); await fs.mkdir(dirPath, { recursive: true }); @@ -254,7 +270,7 @@ function createMcpServer() { ], }; } catch (error) { - console.error(`Failed to create directory ${dirPath}:`, error.message); + logMcpError(logger, error, { dirPath, operation: 'create_directory' }); return { content: [ @@ -281,7 +297,7 @@ function createMcpServer() { } try { - console.info(`Deleting file: ${filePath}`); + logMcpToolCall(logger, 'delete_file', { filePath }); await fs.unlink(filePath); @@ -294,7 +310,7 @@ function createMcpServer() { ], }; } catch (error) { - console.error(`Failed to delete file ${filePath}:`, error.message); + logMcpError(logger, error, { filePath, operation: 'delete_file' }); let errorMessage = `Failed to delete file: ${filePath}\n`; if (error.code === 'ENOENT') { @@ -337,9 +353,10 @@ function createMcpServer() { } try { - console.info( - `Deleting directory: ${dirPath} (recursive: ${recursive})` - ); + logMcpToolCall(logger, 'delete_directory', { + dirPath, + recursive, + }); if (recursive) { await fs.rm(dirPath, { recursive: true, force: true }); @@ -356,7 +373,11 @@ function createMcpServer() { ], }; } catch (error) { - console.error(`Failed to delete directory ${dirPath}:`, error.message); + logMcpError(logger, error, { + dirPath, + recursive, + operation: 'delete_directory', + }); let errorMessage = `Failed to delete directory: ${dirPath}\n`; if (error.code === 'ENOENT') { @@ -397,7 +418,7 @@ function createMcpServer() { } try { - console.info(`Getting info for: ${filePath}`); + logMcpToolCall(logger, 'file_info', { filePath }); const stats = await fs.stat(filePath); @@ -436,7 +457,7 @@ function createMcpServer() { ], }; } catch (error) { - console.error(`Failed to get info for ${filePath}:`, error.message); + logMcpError(logger, error, { filePath, operation: 'file_info' }); let errorMessage = `Failed to get file info: ${filePath}\n`; if (error.code === 'ENOENT') { @@ -469,9 +490,7 @@ function setupSessionRoutes() { // Handle POST requests for MCP communication app.post('/mcp', async (req, res) => { try { - console.info('MCP POST request received:'); - console.info(' Headers: %s', JSON.stringify(req.headers, null, 2)); - console.info(' Body: %s', JSON.stringify(req.body, null, 2)); + logMcpRequest(logger, req, 'MCP POST request received'); const accept = req.headers.accept || req.headers.Accept; if ( @@ -479,7 +498,7 @@ function setupSessionRoutes() { !accept.includes('application/json') || !accept.includes('text/event-stream') ) { - console.info('Adding missing Accept headers for MCP compatibility'); + logger.debug('Adding missing Accept headers for MCP compatibility'); req.headers.accept = 'application/json, text/event-stream'; } @@ -492,14 +511,14 @@ function setupSessionRoutes() { transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (newSessionId) => { - console.info(`MCP session initialized: ${newSessionId}`); + logMcpSession(logger, 'initialized', { sessionId: newSessionId }); transports[newSessionId] = transport; }, }); transport.onclose = () => { if (transport.sessionId) { - console.info(`MCP session closed: ${transport.sessionId}`); + logMcpSession(logger, 'closed', { sessionId: transport.sessionId }); delete transports[transport.sessionId]; } }; @@ -510,7 +529,7 @@ function setupSessionRoutes() { await transport.handleRequest(req, res, req.body); } catch (error) { - console.error('Error handling MCP request:', error); + logMcpError(logger, error, { operation: 'mcp_request_handling' }); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', @@ -539,7 +558,7 @@ app.get('/health', (req, res) => { activeSessions: Object.keys(transports).length, }; - console.info('Health check requested: %j', healthStatus); + logger.info('Health check requested', healthStatus); res.json(healthStatus); }); @@ -553,34 +572,36 @@ async function startServer() { setupSessionRoutes(); app.listen(port, host, async () => { - console.info(`MCP Filesystem server running on http://${host}:${port}`); - console.info('Protocol: Model Context Protocol (MCP)'); - console.info('Transport: Streamable HTTP'); - console.info('Available endpoints:'); - console.info(' POST /mcp - MCP protocol endpoint'); - console.info( - ' GET /mcp - SSE notifications (with session-id header)' - ); - console.info( - ' DELETE /mcp - Session termination (with session-id header)' - ); - console.info(' GET /health - Health check'); - console.info('Available tools:'); - console.info(' - read_file - Read content from a file'); - console.info(' - write_file - Write content to a file'); - console.info(' - list_directory - List directory contents'); - console.info(' - create_directory - Create a new directory'); - console.info(' - delete_file - Delete a file'); - console.info(' - delete_directory - Delete a directory'); - console.info(' - file_info - Get file or directory information'); - console.info('Allowed directories:', allowedDirectories); - - console.info('MCP Filesystem server ready for connections'); + logger.info('MCP Filesystem server started', { + host, + port, + protocol: 'Model Context Protocol (MCP)', + transport: 'Streamable HTTP', + endpoints: { + mcp: 'POST /mcp - MCP protocol endpoint', + mcpSSE: 'GET /mcp - SSE notifications (with session-id header)', + mcpTerminate: + 'DELETE /mcp - Session termination (with session-id header)', + health: 'GET /health - Health check', + }, + tools: [ + 'read_file - Read content from a file', + 'write_file - Write content to a file', + 'list_directory - List directory contents', + 'create_directory - Create a new directory', + 'delete_file - Delete a file', + 'delete_directory - Delete a directory', + 'file_info - Get file or directory information', + ], + allowedDirectories, + }); + + logger.info('MCP Filesystem server ready for connections'); }); } process.on('SIGTERM', () => { - console.info('Received SIGTERM, shutting down gracefully'); + logger.info('Received SIGTERM, shutting down gracefully'); Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); }); @@ -588,7 +609,7 @@ process.on('SIGTERM', () => { }); process.on('SIGINT', () => { - console.info('Received SIGINT, shutting down gracefully'); + logger.info('Received SIGINT, shutting down gracefully'); Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); }); @@ -596,6 +617,6 @@ process.on('SIGINT', () => { }); startServer().catch((error) => { - console.error('Failed to start server:', error); + logMcpError(logger, error, { operation: 'server_startup' }); process.exit(1); }); diff --git a/examples/mcp/mcp-servers/filesystem/logger.js b/examples/mcp/mcp-servers/filesystem/logger.js new file mode 100644 index 0000000..db86d11 --- /dev/null +++ b/examples/mcp/mcp-servers/filesystem/logger.js @@ -0,0 +1,97 @@ +/** + * Standardized Winston Logger for MCP Servers + * + * Provides consistent, single-line logging across all MCP servers + * with structured metadata and unified formatting. + */ + +import winston from 'winston'; + +/** + * Create a standardized logger for MCP servers + * @param {string} serviceName - The name of the MCP service + * @param {string} [version='1.0.0'] - The version of the service + * @param {string} [logLevel] - Override log level (defaults to LOG_LEVEL env var or 'info') + * @returns {winston.Logger} Configured Winston logger + */ +export function createMcpLogger(serviceName, version = '1.0.0', logLevel) { + const level = logLevel || process.env.LOG_LEVEL || 'info'; + + return winston.createLogger({ + level, + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }), + winston.format.errors({ stack: true }), + winston.format.json() + ), + defaultMeta: { + service: serviceName, + version, + protocol: 'Model Context Protocol', + transport: 'Streamable HTTP', + }, + transports: [ + new winston.transports.Console({ + handleExceptions: true, + handleRejections: true, + }), + ], + }); +} + +/** + * Log MCP request received + */ +export function logMcpRequest(logger, req, additionalMeta = {}) { + const sessionId = req.headers['mcp-session-id']; + const method = req.body?.method; + const id = req.body?.id; + + logger.info('MCP request received', { + sessionId, + method, + requestId: id, + userAgent: req.headers['user-agent'], + contentLength: req.headers['content-length'], + ...additionalMeta, + }); +} + +/** + * Log MCP session events + */ +export function logMcpSession(logger, event, sessionId, additionalMeta = {}) { + logger.info(`MCP session ${event}`, { + sessionId, + ...additionalMeta, + }); +} + +/** + * Log MCP tool calls + */ +export function logMcpToolCall( + logger, + toolName, + sessionId, + args = {}, + additionalMeta = {} +) { + logger.info(`MCP tool called: ${toolName}`, { + sessionId, + tool: toolName, + args: Object.keys(args), + ...additionalMeta, + }); +} + +/** + * Log MCP errors with context + */ +export function logMcpError(logger, error, context = {}) { + logger.error('MCP error occurred', { + error: error.message, + stack: error.stack, + ...context, + }); +} diff --git a/examples/mcp/mcp-servers/filesystem/package-lock.json b/examples/mcp/mcp-servers/filesystem/package-lock.json index eb070a4..2a9002f 100644 --- a/examples/mcp/mcp-servers/filesystem/package-lock.json +++ b/examples/mcp/mcp-servers/filesystem/package-lock.json @@ -12,12 +12,33 @@ "@modelcontextprotocol/sdk": "^1.12.0", "cors": "^2.8.5", "express": "^4.18.2", + "winston": "^3.17.0", "zod": "^3.22.0" }, "engines": { "node": ">=18.0.0" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz", @@ -314,6 +335,12 @@ "node": ">= 0.6" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -349,6 +376,12 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -426,6 +459,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -537,6 +615,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -685,6 +769,12 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -703,6 +793,12 @@ "node": ">= 0.8" } }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -846,12 +942,30 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -864,6 +978,35 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -990,6 +1133,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1096,6 +1248,20 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1164,6 +1330,15 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1323,6 +1498,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1332,6 +1525,21 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1341,6 +1549,15 @@ "node": ">=0.6" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1372,6 +1589,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1405,6 +1628,42 @@ "node": ">= 8" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/examples/mcp/mcp-servers/filesystem/package.json b/examples/mcp/mcp-servers/filesystem/package.json index 93e0406..63db973 100644 --- a/examples/mcp/mcp-servers/filesystem/package.json +++ b/examples/mcp/mcp-servers/filesystem/package.json @@ -12,6 +12,7 @@ "@modelcontextprotocol/sdk": "^1.12.0", "cors": "^2.8.5", "express": "^4.18.2", + "winston": "^3.17.0", "zod": "^3.22.0" }, "engines": { diff --git a/examples/mcp/mcp-servers/memory/index.js b/examples/mcp/mcp-servers/memory/index.js index 8b7c618..4d9d230 100644 --- a/examples/mcp/mcp-servers/memory/index.js +++ b/examples/mcp/mcp-servers/memory/index.js @@ -14,10 +14,20 @@ import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import { promises as fs } from 'node:fs'; import path from 'node:path'; +import { + createMcpLogger, + logMcpRequest, + logMcpSession, + logMcpToolCall, + logMcpError, +} from './logger.js'; const app = express(); app.use(express.json()); +// Create standardized logger +const logger = createMcpLogger('mcp-memory', '1.0.0'); + const transports = {}; const memoryDir = process.env.MEMORY_DIR || '/tmp/memory'; diff --git a/examples/mcp/mcp-servers/memory/logger.js b/examples/mcp/mcp-servers/memory/logger.js new file mode 100644 index 0000000..db86d11 --- /dev/null +++ b/examples/mcp/mcp-servers/memory/logger.js @@ -0,0 +1,97 @@ +/** + * Standardized Winston Logger for MCP Servers + * + * Provides consistent, single-line logging across all MCP servers + * with structured metadata and unified formatting. + */ + +import winston from 'winston'; + +/** + * Create a standardized logger for MCP servers + * @param {string} serviceName - The name of the MCP service + * @param {string} [version='1.0.0'] - The version of the service + * @param {string} [logLevel] - Override log level (defaults to LOG_LEVEL env var or 'info') + * @returns {winston.Logger} Configured Winston logger + */ +export function createMcpLogger(serviceName, version = '1.0.0', logLevel) { + const level = logLevel || process.env.LOG_LEVEL || 'info'; + + return winston.createLogger({ + level, + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }), + winston.format.errors({ stack: true }), + winston.format.json() + ), + defaultMeta: { + service: serviceName, + version, + protocol: 'Model Context Protocol', + transport: 'Streamable HTTP', + }, + transports: [ + new winston.transports.Console({ + handleExceptions: true, + handleRejections: true, + }), + ], + }); +} + +/** + * Log MCP request received + */ +export function logMcpRequest(logger, req, additionalMeta = {}) { + const sessionId = req.headers['mcp-session-id']; + const method = req.body?.method; + const id = req.body?.id; + + logger.info('MCP request received', { + sessionId, + method, + requestId: id, + userAgent: req.headers['user-agent'], + contentLength: req.headers['content-length'], + ...additionalMeta, + }); +} + +/** + * Log MCP session events + */ +export function logMcpSession(logger, event, sessionId, additionalMeta = {}) { + logger.info(`MCP session ${event}`, { + sessionId, + ...additionalMeta, + }); +} + +/** + * Log MCP tool calls + */ +export function logMcpToolCall( + logger, + toolName, + sessionId, + args = {}, + additionalMeta = {} +) { + logger.info(`MCP tool called: ${toolName}`, { + sessionId, + tool: toolName, + args: Object.keys(args), + ...additionalMeta, + }); +} + +/** + * Log MCP errors with context + */ +export function logMcpError(logger, error, context = {}) { + logger.error('MCP error occurred', { + error: error.message, + stack: error.stack, + ...context, + }); +} diff --git a/examples/mcp/mcp-servers/memory/package-lock.json b/examples/mcp/mcp-servers/memory/package-lock.json new file mode 100644 index 0000000..49cf410 --- /dev/null +++ b/examples/mcp/mcp-servers/memory/package-lock.json @@ -0,0 +1,1692 @@ +{ + "name": "mcp-memory-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-memory-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "cors": "^2.8.5", + "express": "^4.18.2", + "winston": "^3.17.0", + "zod": "^3.22.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz", + "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz", + "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.46", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.46.tgz", + "integrity": "sha512-IqRxcHEIjqLd4LNS/zKffB3Jzg3NwqJxQQ0Ns7pdrvgGkwQsEBdEQcOHaBVqvvZArShRzI39+aMST3FBGmTrLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/examples/mcp/mcp-servers/memory/package.json b/examples/mcp/mcp-servers/memory/package.json index df4a9f3..3b6ded0 100644 --- a/examples/mcp/mcp-servers/memory/package.json +++ b/examples/mcp/mcp-servers/memory/package.json @@ -12,6 +12,7 @@ "@modelcontextprotocol/sdk": "^1.12.0", "cors": "^2.8.5", "express": "^4.18.2", + "winston": "^3.17.0", "zod": "^3.22.0" }, "engines": { diff --git a/examples/mcp/mcp-servers/npm/index.js b/examples/mcp/mcp-servers/npm/index.js index 20d0a37..c231d31 100644 --- a/examples/mcp/mcp-servers/npm/index.js +++ b/examples/mcp/mcp-servers/npm/index.js @@ -16,9 +16,19 @@ import { z } from 'zod'; import { exec } from 'node:child_process'; import { promisify } from 'node:util'; import path from 'node:path'; +import { + createMcpLogger, + logMcpRequest, + logMcpSession, + logMcpToolCall, + logMcpError, +} from './logger.js'; const execAsync = promisify(exec); +// Create standardized logger +const logger = createMcpLogger('mcp-npm', '1.0.0'); + // Express app for HTTP transport const app = express(); app.use(express.json()); @@ -49,9 +59,10 @@ const ALLOWED_NPM_COMMANDS = [ 'npx', ]; -console.info('NPM MCP Server starting...'); -console.info('Working directory:', workingDirectory); -console.info('Allowed npm commands:', ALLOWED_NPM_COMMANDS); +logger.info('NPM MCP Server starting', { + workingDirectory, + allowedCommands: ALLOWED_NPM_COMMANDS, +}); /** * Validate npm command for security @@ -88,7 +99,10 @@ async function executeNpmCommand(command, cwd = workingDirectory) { const validatedCommand = validateNpmCommand(command); const fullCommand = `npm ${validatedCommand}`; - console.info(`Executing npm command: ${fullCommand} in ${cwd}`); + logger.info('Executing npm command', { + command: fullCommand, + workingDirectory: cwd, + }); try { const { stdout, stderr } = await execAsync(fullCommand, { @@ -105,7 +119,10 @@ async function executeNpmCommand(command, cwd = workingDirectory) { cwd, }; } catch (error) { - console.error(`NPM command failed: ${fullCommand}`, error.message); + logMcpError(logger, error, { + command: fullCommand, + workingDirectory: cwd, + }); return { success: false, @@ -142,6 +159,8 @@ function createMcpServer() { async ({ command, cwd }) => { const workDir = cwd || workingDirectory; + logMcpToolCall(logger, 'npm_run', 'unknown', { command, cwd: workDir }); + try { const result = await executeNpmCommand(command, workDir); @@ -170,10 +189,11 @@ function createMcpServer() { ], }; } catch (error) { - console.error( - `Failed to execute npm command: ${command}`, - error.message - ); + logMcpError(logger, error, { + tool: 'npm_run', + command, + workingDirectory: workDir, + }); return { content: [ @@ -202,6 +222,12 @@ function createMcpServer() { const workDir = cwd || workingDirectory; const projectDir = path.join(workDir, name); + logMcpToolCall(logger, 'npm_init', 'unknown', { + name, + cwd: workDir, + yes, + }); + try { // Create project directory await execAsync(`mkdir -p "${projectDir}"`); @@ -232,10 +258,11 @@ function createMcpServer() { ], }; } catch (error) { - console.error( - `Failed to initialize npm project: ${name}`, - error.message - ); + logMcpError(logger, error, { + tool: 'npm_init', + projectName: name, + workingDirectory: projectDir, + }); return { content: [ @@ -267,6 +294,13 @@ function createMcpServer() { async ({ packages = [], cwd, dev = false, global = false }) => { const workDir = cwd || workingDirectory; + logMcpToolCall(logger, 'npm_install', 'unknown', { + packages, + cwd: workDir, + dev, + global, + }); + try { let command = 'install'; @@ -308,7 +342,13 @@ function createMcpServer() { ], }; } catch (error) { - console.error(`Failed to install npm packages`, error.message); + logMcpError(logger, error, { + tool: 'npm_install', + packages, + workingDirectory: workDir, + dev, + global, + }); return { content: [ @@ -365,6 +405,17 @@ function createMcpServer() { }) => { const workDir = cwd || workingDirectory; + logMcpToolCall(logger, 'create_nextjs_project', 'unknown', { + name, + cwd: workDir, + typescript, + tailwind, + eslint, + appRouter, + srcDir, + importAlias, + }); + try { // Build the npx create-next-app command with options let command = `npx create-next-app@latest "${name}" --yes`; @@ -404,7 +455,19 @@ function createMcpServer() { command += ` --import-alias "${importAlias}"`; } - console.info(`Creating Next.js project: ${command} in ${workDir}`); + logger.info('Creating Next.js project', { + command, + workingDirectory: workDir, + projectName: name, + options: { + typescript, + tailwind, + eslint, + appRouter, + srcDir, + importAlias, + }, + }); const { stdout, stderr } = await execAsync(command, { cwd: workDir, @@ -445,10 +508,11 @@ function createMcpServer() { ], }; } catch (error) { - console.error( - `Failed to create Next.js project: ${name}`, - error.message - ); + logMcpError(logger, error, { + tool: 'create_nextjs_project', + projectName: name, + workingDirectory: workDir, + }); return { content: [ @@ -472,9 +536,7 @@ function setupSessionRoutes() { // Handle POST requests for MCP communication app.post('/mcp', async (req, res) => { try { - console.info('MCP POST request received:'); - console.info(' Headers: %s', JSON.stringify(req.headers, null, 2)); - console.info(' Body: %s', JSON.stringify(req.body, null, 2)); + logMcpRequest(logger, req); // Fix missing Accept headers for compatibility with Go MCP clients const accept = req.headers.accept || req.headers.Accept; @@ -483,7 +545,7 @@ function setupSessionRoutes() { !accept.includes('application/json') || !accept.includes('text/event-stream') ) { - console.info('Adding missing Accept headers for MCP compatibility'); + logger.debug('Adding missing Accept headers for MCP compatibility'); req.headers.accept = 'application/json, text/event-stream'; } @@ -499,7 +561,7 @@ function setupSessionRoutes() { transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (newSessionId) => { - console.info(`MCP session initialized: ${newSessionId}`); + logMcpSession(logger, 'initialized', newSessionId); // Store the transport by session ID transports[newSessionId] = transport; }, @@ -508,7 +570,7 @@ function setupSessionRoutes() { // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { - console.info(`MCP session closed: ${transport.sessionId}`); + logMcpSession(logger, 'closed', transport.sessionId); delete transports[transport.sessionId]; } }; @@ -521,7 +583,10 @@ function setupSessionRoutes() { // Handle the MCP request await transport.handleRequest(req, res, req.body); } catch (error) { - console.error('Error handling MCP request:', error); + logMcpError(logger, error, { + endpoint: '/mcp', + method: 'POST', + }); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', @@ -587,31 +652,30 @@ async function startServer() { setupHealthCheck(); app.listen(port, '0.0.0.0', () => { - console.info(''); - console.info('🚀 MCP NPM Server Started Successfully!'); - console.info('=========================================='); - console.info(`Server running on: http://0.0.0.0:${port}`); - console.info(''); - console.info('Available endpoints:'); - console.info(' POST /mcp - MCP protocol communication'); - console.info(' GET /mcp - MCP SSE notifications'); - console.info(' DELETE /mcp - MCP session termination'); - console.info(' GET /health - Health check'); - console.info('Available tools:'); - console.info(' - npm_run - Run any whitelisted npm command'); - console.info(' - npm_init - Initialize a new npm project'); - console.info(' - npm_install - Install npm packages'); - console.info(' - create_nextjs_project - Create a new Next.js project'); - console.info('Working directory:', workingDirectory); - console.info('Allowed npm commands:', ALLOWED_NPM_COMMANDS); - console.info(''); - console.info('MCP NPM server ready for connections'); + logger.info('MCP NPM Server started successfully', { + host: '0.0.0.0', + port, + endpoints: { + mcp: 'POST /mcp - MCP protocol communication', + mcpSse: 'GET /mcp - MCP SSE notifications', + mcpDelete: 'DELETE /mcp - MCP session termination', + health: 'GET /health - Health check', + }, + tools: [ + 'npm_run - Run any whitelisted npm command', + 'npm_init - Initialize a new npm project', + 'npm_install - Install npm packages', + 'create_nextjs_project - Create a new Next.js project', + ], + workingDirectory, + allowedCommands: ALLOWED_NPM_COMMANDS, + }); }); } // Graceful shutdown process.on('SIGTERM', () => { - console.info('Received SIGTERM, shutting down gracefully'); + logger.info('Received SIGTERM, shutting down gracefully'); // Close all transports Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); @@ -620,7 +684,7 @@ process.on('SIGTERM', () => { }); process.on('SIGINT', () => { - console.info('Received SIGINT, shutting down gracefully'); + logger.info('Received SIGINT, shutting down gracefully'); // Close all transports Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); @@ -630,6 +694,8 @@ process.on('SIGINT', () => { // Start the server startServer().catch((error) => { - console.error('Failed to start server:', error); + logMcpError(logger, error, { + operation: 'server-startup', + }); process.exit(1); }); diff --git a/examples/mcp/mcp-servers/npm/logger.js b/examples/mcp/mcp-servers/npm/logger.js new file mode 100644 index 0000000..db86d11 --- /dev/null +++ b/examples/mcp/mcp-servers/npm/logger.js @@ -0,0 +1,97 @@ +/** + * Standardized Winston Logger for MCP Servers + * + * Provides consistent, single-line logging across all MCP servers + * with structured metadata and unified formatting. + */ + +import winston from 'winston'; + +/** + * Create a standardized logger for MCP servers + * @param {string} serviceName - The name of the MCP service + * @param {string} [version='1.0.0'] - The version of the service + * @param {string} [logLevel] - Override log level (defaults to LOG_LEVEL env var or 'info') + * @returns {winston.Logger} Configured Winston logger + */ +export function createMcpLogger(serviceName, version = '1.0.0', logLevel) { + const level = logLevel || process.env.LOG_LEVEL || 'info'; + + return winston.createLogger({ + level, + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }), + winston.format.errors({ stack: true }), + winston.format.json() + ), + defaultMeta: { + service: serviceName, + version, + protocol: 'Model Context Protocol', + transport: 'Streamable HTTP', + }, + transports: [ + new winston.transports.Console({ + handleExceptions: true, + handleRejections: true, + }), + ], + }); +} + +/** + * Log MCP request received + */ +export function logMcpRequest(logger, req, additionalMeta = {}) { + const sessionId = req.headers['mcp-session-id']; + const method = req.body?.method; + const id = req.body?.id; + + logger.info('MCP request received', { + sessionId, + method, + requestId: id, + userAgent: req.headers['user-agent'], + contentLength: req.headers['content-length'], + ...additionalMeta, + }); +} + +/** + * Log MCP session events + */ +export function logMcpSession(logger, event, sessionId, additionalMeta = {}) { + logger.info(`MCP session ${event}`, { + sessionId, + ...additionalMeta, + }); +} + +/** + * Log MCP tool calls + */ +export function logMcpToolCall( + logger, + toolName, + sessionId, + args = {}, + additionalMeta = {} +) { + logger.info(`MCP tool called: ${toolName}`, { + sessionId, + tool: toolName, + args: Object.keys(args), + ...additionalMeta, + }); +} + +/** + * Log MCP errors with context + */ +export function logMcpError(logger, error, context = {}) { + logger.error('MCP error occurred', { + error: error.message, + stack: error.stack, + ...context, + }); +} diff --git a/examples/mcp/mcp-servers/npm/package-lock.json b/examples/mcp/mcp-servers/npm/package-lock.json new file mode 100644 index 0000000..3bbf2c7 --- /dev/null +++ b/examples/mcp/mcp-servers/npm/package-lock.json @@ -0,0 +1,1692 @@ +{ + "name": "mcp-npm-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-npm-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "cors": "^2.8.5", + "express": "^4.18.2", + "winston": "^3.17.0", + "zod": "^3.22.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz", + "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz", + "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.46", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.46.tgz", + "integrity": "sha512-IqRxcHEIjqLd4LNS/zKffB3Jzg3NwqJxQQ0Ns7pdrvgGkwQsEBdEQcOHaBVqvvZArShRzI39+aMST3FBGmTrLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/examples/mcp/mcp-servers/npm/package.json b/examples/mcp/mcp-servers/npm/package.json index 120fe24..d7a68e6 100644 --- a/examples/mcp/mcp-servers/npm/package.json +++ b/examples/mcp/mcp-servers/npm/package.json @@ -12,6 +12,7 @@ "@modelcontextprotocol/sdk": "^1.12.0", "cors": "^2.8.5", "express": "^4.18.2", + "winston": "^3.17.0", "zod": "^3.22.0" }, "engines": { diff --git a/examples/mcp/mcp-servers/web-search/index-http.js b/examples/mcp/mcp-servers/web-search/index-http.js deleted file mode 100644 index 513d933..0000000 --- a/examples/mcp/mcp-servers/web-search/index-http.js +++ /dev/null @@ -1,331 +0,0 @@ -/** - * HTTP-based Web Search Server using MCP SDK - * - * This server uses the official MCP TypeScript SDK with StreamableHTTPServerTransport - * in stateless mode, which responds with plain HTTP JSON-RPC instead of SSE. - * This is compatible with Go MCP clients that expect standard HTTP responses. - */ - -import express from 'express'; -import axios from 'axios'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { z } from 'zod'; - -const app = express(); -app.use(express.json()); - -/** - * Generate simulated search results - */ -function generateSearchResults(query, limit) { - const results = []; - const domains = [ - 'example.com', - 'wikipedia.org', - 'github.com', - 'stackoverflow.com', - 'medium.com', - ]; - - for (let i = 0; i < Math.min(limit, 10); i++) { - const domain = domains[i % domains.length]; - const title = `${query} - Result ${i + 1}`; - const url = `https://${domain}/${query - .toLowerCase() - .replace(/\s+/g, '-')}-${i + 1}`; - const description = `This is a simulated search result for "${query}". It would normally contain relevant information about your search query.`; - - results.push(`${i + 1}. ${title}\n ${url}\n ${description}\n`); - } - - return results.join('\n'); -} - -/** - * Create and configure the MCP server - */ -function createServer() { - const server = new McpServer( - { - name: 'web-search-server', - version: '1.0.0', - }, - { - capabilities: { - tools: {}, - }, - } - ); - - // Add fetch_url tool - server.tool( - 'fetch_url', - { - description: 'Fetch content from a URL', - inputSchema: { - type: 'object', - properties: { - url: z.string().url(), - timeout: z.number().min(1000).max(30000).optional().default(10000), - }, - required: ['url'], - }, - }, - async ({ url, timeout = 10000 }) => { - try { - console.info(`Fetching URL: ${url}`); - const response = await axios.get(url, { - timeout, - headers: { - 'User-Agent': 'HTTP-Web-Search-Server/1.0.0', - }, - maxRedirects: 5, - validateStatus: (status) => status < 500, - }); - - const contentType = response.headers['content-type'] || ''; - let content; - if (contentType.includes('application/json')) { - content = JSON.stringify(response.data, null, 2); - } else if (contentType.includes('text/')) { - content = response.data.toString(); - } else { - content = `Binary content (${contentType}), size: ${ - JSON.stringify(response.data).length - } bytes`; - } - - // Truncate very large responses - if (content.length > 10000) { - content = content.substring(0, 10000) + '\n\n... (content truncated)'; - } - - return { - content: [ - { - type: 'text', - text: `URL: ${url}\nStatus: ${response.status} ${response.statusText}\nContent-Type: ${contentType}\n\nContent:\n${content}`, - }, - ], - }; - } catch (error) { - console.error(`Failed to fetch URL ${url}:`, error.message); - let errorMessage = `Failed to fetch URL: ${url}\n`; - if (error.code === 'ENOTFOUND') { - errorMessage += 'Domain not found'; - } else if (error.code === 'ECONNREFUSED') { - errorMessage += 'Connection refused'; - } else if (error.code === 'ETIMEDOUT') { - errorMessage += 'Request timed out'; - } else if (error.response) { - errorMessage += `HTTP ${error.response.status}: ${error.response.statusText}`; - } else { - errorMessage += error.message; - } - - return { - content: [ - { - type: 'text', - text: errorMessage, - }, - ], - }; - } - } - ); - - // Add search_web tool - server.tool( - 'search_web', - { - description: 'Perform a web search', - inputSchema: { - type: 'object', - properties: { - query: z.string().min(1).max(500), - limit: z.number().min(1).max(20).optional().default(5), - }, - required: ['query'], - }, - }, - async ({ query, limit = 5 }) => { - console.info(`Searching for: "${query}" (limit: ${limit})`); - const searchResults = generateSearchResults(query, limit); - - return { - content: [ - { - type: 'text', - text: `Search Results for "${query}":\n\n${searchResults}`, - }, - ], - }; - } - ); - - // Add get_page_title tool - server.tool( - 'get_page_title', - { - description: 'Extract title from a web page', - inputSchema: { - type: 'object', - properties: { - url: z.string().url(), - }, - required: ['url'], - }, - }, - async ({ url }) => { - try { - console.info(`Extracting title from: ${url}`); - const response = await axios.get(url, { - timeout: 10000, - headers: { - 'User-Agent': 'HTTP-Web-Search-Server/1.0.0', - }, - }); - - const titleMatch = response.data.match(/]*>([^<]+)<\/title>/i); - const title = titleMatch ? titleMatch[1].trim() : 'No title found'; - - return { - content: [ - { - type: 'text', - text: `Title: ${title}\nURL: ${url}`, - }, - ], - }; - } catch (error) { - console.error(`Failed to extract title from ${url}:`, error.message); - - return { - content: [ - { - type: 'text', - text: `Failed to extract title from: ${url}\nError: ${error.message}`, - }, - ], - }; - } - } - ); - - return server; -} - -/** - * Handle MCP requests using stateless mode - */ -app.post('/mcp', async (req, res) => { - try { - console.info('HTTP JSON-RPC request received:'); - console.info(' Body: %s', JSON.stringify(req.body, null, 2)); - - // Create new server and transport instances for each request (stateless mode) - const server = createServer(); - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, // Stateless mode - }); - - // Clean up on request close - res.on('close', () => { - console.info('Request closed'); - transport.close(); - server.close(); - }); - - // Connect server to transport - await server.connect(transport); - - // Handle the request - await transport.handleRequest(req, res, req.body); - } catch (error) { - console.error('Error handling MCP request:', error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal server error', - data: error.message, - }, - id: req.body?.id || null, - }); - } - } -}); - -// Handle unsupported methods for stateless mode -app.get('/mcp', async (req, res) => { - console.info('Received GET MCP request'); - res.status(405).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Method not allowed in stateless mode.', - }, - id: null, - }); -}); - -app.delete('/mcp', async (req, res) => { - console.info('Received DELETE MCP request'); - res.status(405).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Method not allowed in stateless mode.', - }, - id: null, - }); -}); - -// Health check endpoint -app.get('/health', (req, res) => { - const healthStatus = { - status: 'healthy', - timestamp: new Date().toISOString(), - uptime: process.uptime(), - service: 'http-web-search', - version: '1.0.0', - protocol: 'HTTP JSON-RPC', - }; - - console.info('Health check requested: %j', healthStatus); - res.json(healthStatus); -}); - -// Start the server -const port = process.env.PORT || 3001; -const host = process.env.HOST || '0.0.0.0'; - -app.listen(port, host, () => { - console.info(`HTTP Web Search server running on http://${host}:${port}`); - console.info('Protocol: HTTP JSON-RPC 2.0'); - console.info('Available endpoints:'); - console.info(' POST /mcp - JSON-RPC endpoint'); - console.info(' GET /health - Health check'); - console.info('Available methods:'); - console.info(' - initialize - Initialize the server'); - console.info(' - tools/list - List available tools'); - console.info(' - tools/call - Call a tool'); - console.info('Available tools:'); - console.info(' - fetch_url - Fetch content from a URL'); - console.info(' - search_web - Perform web search (simulated)'); - console.info(' - get_page_title - Extract title from a web page'); -}); - -// Graceful shutdown -process.on('SIGTERM', () => { - console.info('Received SIGTERM, shutting down gracefully'); - process.exit(0); -}); - -process.on('SIGINT', () => { - console.info('Received SIGINT, shutting down gracefully'); - process.exit(0); -}); diff --git a/examples/mcp/mcp-servers/web-search/index.js b/examples/mcp/mcp-servers/web-search/index.js index eadea5b..4a0a4bf 100644 --- a/examples/mcp/mcp-servers/web-search/index.js +++ b/examples/mcp/mcp-servers/web-search/index.js @@ -12,24 +12,84 @@ import express from 'express'; import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import axios from 'axios'; +import * as cheerio from 'cheerio'; +import { + createMcpLogger, + logMcpRequest, + logMcpSession, + logMcpToolCall, + logMcpError, +} from './logger.js'; + +const logger = createMcpLogger('mcp-web-search', '1.0.0'); -// Express app for HTTP transport const app = express(); -app.use(express.json()); +app.use(express.json({ limit: '10mb' })); + +app.use((req, res, next) => { + const startTime = Date.now(); + const requestId = randomUUID(); + + req.requestId = requestId; + + logger.info('Incoming request', { + requestId, + method: req.method, + url: req.url, + userAgent: req.get('User-Agent'), + contentType: req.get('Content-Type'), + sessionId: req.headers['mcp-session-id'], + }); + + res.on('finish', () => { + const duration = Date.now() - startTime; + logger.info('Request completed', { + requestId, + method: req.method, + url: req.url, + statusCode: res.statusCode, + duration, + sessionId: req.headers['mcp-session-id'], + }); + }); + + next(); +}); -// Map to store transports by session ID -const transports = {}; +const transports = new Map(); + +/** + * Enhanced error handling for tools + */ +function createToolError(error, context = {}) { + logger.error('Tool execution error', { + error: error.message, + stack: error.stack, + context, + }); + + return { + content: [ + { + type: 'text', + text: `Error: ${error.message}`, + }, + ], + isError: true, + }; +} /** * Create and configure the MCP server */ function createMcpServer() { + logger.info('Creating MCP server instance'); + const mcpServer = new McpServer({ name: 'web-search', version: '1.0.0', }); - // Tool: Fetch URL content mcpServer.tool( 'fetch_url', { @@ -40,25 +100,79 @@ function createMcpServer() { .max(30000) .default(10000) .describe('Request timeout in milliseconds'), + extract_text: z + .boolean() + .default(true) + .describe('Extract plain text from HTML content'), }, - async ({ url, timeout = 10000 }) => { + async ({ url, timeout = 10000, extract_text = true }) => { + const operationId = randomUUID(); + try { - console.info(`Fetching URL: ${url}`); + logger.info('Fetching URL', { + operationId, + url, + timeout, + extract_text, + }); const response = await axios.get(url, { timeout, headers: { - 'User-Agent': 'MCP-Web-Search-Server/1.0.0', + 'User-Agent': 'MCP-Web-Search-Server/1.0.0 (Compatible)', + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate', + Connection: 'keep-alive', }, maxRedirects: 5, - validateStatus: (status) => status < 500, // Accept 4xx but not 5xx + validateStatus: (status) => status < 500, }); const contentType = response.headers['content-type'] || ''; let content; + logger.info('URL fetch successful', { + operationId, + url, + statusCode: response.status, + contentType, + contentLength: response.data?.length || 0, + }); + if (contentType.includes('application/json')) { content = JSON.stringify(response.data, null, 2); + } else if (contentType.includes('text/html') && extract_text) { + const rawContent = response.data.toString(); + + try { + const $ = cheerio.load(rawContent); + + $('script, style, nav, footer, aside').remove(); + + const title = $('title').text().trim(); + + const mainContent = $( + 'main, article, .content, #content, .main' + ).first(); + const extractedText = + mainContent.length > 0 ? mainContent.text() : $('body').text(); + + const cleanText = extractedText + .replace(/\s+/g, ' ') + .replace(/\n\s*\n/g, '\n') + .trim(); + + content = `Title: ${title}\n\nContent:\n${cleanText}`; + } catch (parseError) { + logger.warn('Failed to parse HTML, returning raw content', { + operationId, + url, + error: parseError.message, + }); + content = rawContent; + } } else if (contentType.includes('text/')) { content = response.data.toString(); } else { @@ -67,49 +181,63 @@ function createMcpServer() { } bytes`; } - // Truncate very large responses - if (content.length > 10000) { - content = content.substring(0, 10000) + '\n\n... (content truncated)'; + const maxLength = 15000; + if (content.length > maxLength) { + content = + content.substring(0, maxLength) + + '\n\n... (content truncated due to length)'; + logger.info('Content truncated', { + operationId, + originalLength: content.length + (content.length - maxLength), + truncatedLength: content.length, + }); } return { content: [ { type: 'text', - text: `URL: ${url}\nStatus: ${response.status} ${response.statusText}\nContent-Type: ${contentType}\n\nContent:\n${content}`, + text: `URL: ${url}\nStatus: ${response.status} ${response.statusText}\nContent-Type: ${contentType}\nExtracted Text: ${extract_text}\n\n${content}`, }, ], }; } catch (error) { - console.error(`Failed to fetch URL ${url}:`, error.message); + logger.error('Failed to fetch URL', { + operationId, + url, + error: error.message, + code: error.code, + status: error.response?.status, + }); let errorMessage = `Failed to fetch URL: ${url}\n`; - if (error.code === 'ENOTFOUND') { - errorMessage += 'Domain not found'; - } else if (error.code === 'ECONNREFUSED') { - errorMessage += 'Connection refused'; - } else if (error.code === 'ETIMEDOUT') { - errorMessage += 'Request timed out'; - } else if (error.response) { - errorMessage += `HTTP ${error.response.status}: ${error.response.statusText}`; - } else { - errorMessage += error.message; + switch (error.code) { + case 'ENOTFOUND': + errorMessage += 'Domain not found or DNS resolution failed'; + break; + case 'ECONNREFUSED': + errorMessage += 'Connection refused by the server'; + break; + case 'ETIMEDOUT': + errorMessage += 'Request timed out'; + break; + case 'ECONNRESET': + errorMessage += 'Connection was reset by the server'; + break; + default: + if (error.response) { + errorMessage += `HTTP ${error.response.status}: ${error.response.statusText}`; + } else { + errorMessage += error.message; + } } - return { - content: [ - { - type: 'text', - text: errorMessage, - }, - ], - }; + return createToolError(new Error(errorMessage), { url, operationId }); } } ); - // Tool: Web search (simulated) mcpServer.tool( 'search_web', { @@ -122,9 +250,8 @@ function createMcpServer() { .describe('Maximum number of results to return'), }, async ({ query, limit = 5 }) => { - console.info(`Searching for: "${query}" (limit: ${limit})`); + logger.info('Performing web search', { query, limit }); - // Generate simulated search results const searchResults = generateSearchResults(query, limit); return { @@ -138,7 +265,6 @@ function createMcpServer() { } ); - // Tool: Get page title mcpServer.tool( 'get_page_title', { @@ -146,7 +272,7 @@ function createMcpServer() { }, async ({ url }) => { try { - console.info(`Extracting title from: ${url}`); + logger.info('Extracting page title', { url }); const response = await axios.get(url, { timeout: 10000, @@ -155,7 +281,6 @@ function createMcpServer() { }, }); - // Simple title extraction using regex (cheerio would require additional dependency) const titleMatch = response.data.match(/]*>([^<]+)<\/title>/i); const title = titleMatch ? titleMatch[1].trim() : 'No title found'; @@ -168,7 +293,10 @@ function createMcpServer() { ], }; } catch (error) { - console.error(`Failed to extract title from ${url}:`, error.message); + logger.error('Failed to extract page title', { + url, + error: error.message, + }); return { content: [ @@ -212,60 +340,51 @@ function generateSearchResults(query, limit) { return results.join('\n'); } -// Handle POST requests for MCP communication app.post('/mcp', async (req, res) => { try { - console.info('MCP POST request received:'); - console.info(' Headers: %s', JSON.stringify(req.headers, null, 2)); - console.info(' Body: %s', JSON.stringify(req.body, null, 2)); + logger.info('MCP POST request received', { + headers: req.headers, + bodyKeys: Object.keys(req.body || {}), + }); - // Fix missing Accept headers for compatibility with Go MCP clients - // The StreamableHTTPServerTransport requires both application/json and text/event-stream const accept = req.headers.accept || req.headers.Accept; if ( !accept || !accept.includes('application/json') || !accept.includes('text/event-stream') ) { - console.info('Adding missing Accept headers for MCP compatibility'); + logger.info('Adding missing Accept headers for MCP compatibility'); req.headers.accept = 'application/json, text/event-stream'; } - // Check for existing session ID const sessionId = req.headers['mcp-session-id']; let transport; if (sessionId && transports[sessionId]) { - // Reuse existing transport transport = transports[sessionId]; } else { - // Create new transport for new session transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (newSessionId) => { - console.info(`MCP session initialized: ${newSessionId}`); - // Store the transport by session ID + logger.info('MCP session initialized', { sessionId: newSessionId }); transports[newSessionId] = transport; }, }); - // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { - console.info(`MCP session closed: ${transport.sessionId}`); + logger.info('MCP session closed', { sessionId: transport.sessionId }); delete transports[transport.sessionId]; } }; - // Create and connect MCP server const server = createMcpServer(); await server.connect(transport); } - // Handle the MCP request await transport.handleRequest(req, res, req.body); } catch (error) { - console.error('Error handling MCP request:', error); + logger.error('Error handling MCP request', { error: error.message }); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', @@ -279,7 +398,6 @@ app.post('/mcp', async (req, res) => { } }); -// Handle GET requests for SSE (server-to-client notifications) app.get('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !transports[sessionId]) { @@ -291,7 +409,6 @@ app.get('/mcp', async (req, res) => { await transport.handleRequest(req, res); }); -// Handle DELETE requests for session termination app.delete('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !transports[sessionId]) { @@ -303,7 +420,6 @@ app.delete('/mcp', async (req, res) => { await transport.handleRequest(req, res); }); -// Health check endpoint app.get('/health', (req, res) => { const healthStatus = { status: 'healthy', @@ -315,38 +431,35 @@ app.get('/health', (req, res) => { transport: 'Streamable HTTP', }; - console.info('Health check requested: %j', healthStatus); + logger.info('Health check requested', healthStatus); res.json(healthStatus); }); -// Start the server const port = process.env.PORT || 3001; const host = process.env.HOST || '0.0.0.0'; app.listen(port, host, () => { - console.info(`MCP Web Search server running on http://${host}:${port}`); - console.info('Protocol: Model Context Protocol (MCP)'); - console.info('Transport: Streamable HTTP'); - console.info('Available endpoints:'); - console.info(' POST /mcp - MCP protocol endpoint'); - console.info( + logger.info(`MCP Web Search server running on http://${host}:${port}`); + logger.info('Protocol: Model Context Protocol (MCP)'); + logger.info('Transport: Streamable HTTP'); + logger.info('Available endpoints:'); + logger.info(' POST /mcp - MCP protocol endpoint'); + logger.info( ' GET /mcp - SSE notifications (with session-id header)' ); - console.info( + logger.info( ' DELETE /mcp - Session termination (with session-id header)' ); - console.info(' GET /health - Health check'); - console.info('Available tools:'); - console.info(' - fetch_url - Fetch content from a URL'); - console.info(' - search_web - Perform web search (simulated)'); - console.info(' - get_page_title - Extract title from a web page'); + logger.info(' GET /health - Health check'); + logger.info('Available tools:'); + logger.info(' - fetch_url - Fetch content from a URL'); + logger.info(' - search_web - Perform web search (simulated)'); + logger.info(' - get_page_title - Extract title from a web page'); }); -// Graceful shutdown process.on('SIGTERM', () => { - console.info('Received SIGTERM, shutting down gracefully'); - // Close all transports + logger.info('Received SIGTERM, shutting down gracefully'); Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); }); @@ -354,8 +467,7 @@ process.on('SIGTERM', () => { }); process.on('SIGINT', () => { - console.info('Received SIGINT, shutting down gracefully'); - // Close all transports + logger.info('Received SIGINT, shutting down gracefully'); Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); }); diff --git a/examples/mcp/mcp-servers/web-search/logger.js b/examples/mcp/mcp-servers/web-search/logger.js new file mode 100644 index 0000000..db86d11 --- /dev/null +++ b/examples/mcp/mcp-servers/web-search/logger.js @@ -0,0 +1,97 @@ +/** + * Standardized Winston Logger for MCP Servers + * + * Provides consistent, single-line logging across all MCP servers + * with structured metadata and unified formatting. + */ + +import winston from 'winston'; + +/** + * Create a standardized logger for MCP servers + * @param {string} serviceName - The name of the MCP service + * @param {string} [version='1.0.0'] - The version of the service + * @param {string} [logLevel] - Override log level (defaults to LOG_LEVEL env var or 'info') + * @returns {winston.Logger} Configured Winston logger + */ +export function createMcpLogger(serviceName, version = '1.0.0', logLevel) { + const level = logLevel || process.env.LOG_LEVEL || 'info'; + + return winston.createLogger({ + level, + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }), + winston.format.errors({ stack: true }), + winston.format.json() + ), + defaultMeta: { + service: serviceName, + version, + protocol: 'Model Context Protocol', + transport: 'Streamable HTTP', + }, + transports: [ + new winston.transports.Console({ + handleExceptions: true, + handleRejections: true, + }), + ], + }); +} + +/** + * Log MCP request received + */ +export function logMcpRequest(logger, req, additionalMeta = {}) { + const sessionId = req.headers['mcp-session-id']; + const method = req.body?.method; + const id = req.body?.id; + + logger.info('MCP request received', { + sessionId, + method, + requestId: id, + userAgent: req.headers['user-agent'], + contentLength: req.headers['content-length'], + ...additionalMeta, + }); +} + +/** + * Log MCP session events + */ +export function logMcpSession(logger, event, sessionId, additionalMeta = {}) { + logger.info(`MCP session ${event}`, { + sessionId, + ...additionalMeta, + }); +} + +/** + * Log MCP tool calls + */ +export function logMcpToolCall( + logger, + toolName, + sessionId, + args = {}, + additionalMeta = {} +) { + logger.info(`MCP tool called: ${toolName}`, { + sessionId, + tool: toolName, + args: Object.keys(args), + ...additionalMeta, + }); +} + +/** + * Log MCP errors with context + */ +export function logMcpError(logger, error, context = {}) { + logger.error('MCP error occurred', { + error: error.message, + stack: error.stack, + ...context, + }); +} diff --git a/examples/mcp/mcp-servers/web-search/package-lock.json b/examples/mcp/mcp-servers/web-search/package-lock.json index c795960..2741e26 100644 --- a/examples/mcp/mcp-servers/web-search/package-lock.json +++ b/examples/mcp/mcp-servers/web-search/package-lock.json @@ -14,12 +14,33 @@ "cheerio": "^1.0.0-rc.12", "cors": "^2.8.5", "express": "^4.18.2", + "winston": "^3.17.0", "zod": "^3.22.0" }, "engines": { "node": ">=18.0.0" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz", @@ -304,6 +325,12 @@ "node": ">= 0.6" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -339,6 +366,12 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -493,6 +526,51 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -708,6 +786,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -896,6 +980,12 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -914,6 +1004,12 @@ "node": ">= 0.8" } }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -1126,12 +1222,30 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1144,6 +1258,35 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1282,6 +1425,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -1431,6 +1583,20 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1499,6 +1665,15 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1658,6 +1833,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1667,6 +1860,21 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1676,6 +1884,15 @@ "node": ">=0.6" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1716,6 +1933,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1770,6 +1993,42 @@ "node": ">= 8" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/examples/mcp/mcp-servers/web-search/package.json b/examples/mcp/mcp-servers/web-search/package.json index db7c2f3..0411055 100644 --- a/examples/mcp/mcp-servers/web-search/package.json +++ b/examples/mcp/mcp-servers/web-search/package.json @@ -14,6 +14,7 @@ "cheerio": "^1.0.0-rc.12", "cors": "^2.8.5", "express": "^4.18.2", + "winston": "^3.17.0", "zod": "^3.22.0" }, "engines": { From 6b4c328860a39dec79dfea0bfff80f3edcfc7103 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 17:40:50 +0000 Subject: [PATCH 24/38] refactor: Implement DuckDuckGo search functionality with safe search options Signed-off-by: Eden Reich --- examples/mcp/mcp-servers/web-search/README.md | 13 +- examples/mcp/mcp-servers/web-search/index.js | 130 +++++++++++------- .../mcp-servers/web-search/package-lock.json | 52 +++++++ .../mcp/mcp-servers/web-search/package.json | 1 + 4 files changed, 147 insertions(+), 49 deletions(-) diff --git a/examples/mcp/mcp-servers/web-search/README.md b/examples/mcp/mcp-servers/web-search/README.md index 35cc9b9..a63113a 100644 --- a/examples/mcp/mcp-servers/web-search/README.md +++ b/examples/mcp/mcp-servers/web-search/README.md @@ -1,11 +1,11 @@ # MCP Web Search Server -A Model Context Protocol (MCP) server that provides web search and URL fetching capabilities. +A Model Context Protocol (MCP) server that provides web search and URL fetching capabilities using DuckDuckGo. ## Features - **fetch_url**: Fetch content from any URL with error handling and timeout support -- **search_web**: Simulated web search functionality (can be replaced with real search APIs) +- **search_web**: Real web search functionality using DuckDuckGo with safe search options - **get_page_title**: Extract page titles from web pages using HTML parsing ## Installation @@ -85,11 +85,18 @@ Returns server health status. "name": "search_web", "arguments": { "query": "machine learning tutorials", - "limit": 5 + "limit": 5, + "safe_search": "moderate" } } ``` +#### Safe Search Options + +- `strict`: Strict safe search filtering +- `moderate`: Moderate safe search filtering (default) +- `off`: No safe search filtering + ### Get Page Title ```json diff --git a/examples/mcp/mcp-servers/web-search/index.js b/examples/mcp/mcp-servers/web-search/index.js index 4a0a4bf..adc5669 100644 --- a/examples/mcp/mcp-servers/web-search/index.js +++ b/examples/mcp/mcp-servers/web-search/index.js @@ -13,13 +13,8 @@ import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import axios from 'axios'; import * as cheerio from 'cheerio'; -import { - createMcpLogger, - logMcpRequest, - logMcpSession, - logMcpToolCall, - logMcpError, -} from './logger.js'; +import { search, SafeSearchType } from 'duck-duck-scrape'; +import { createMcpLogger } from './logger.js'; const logger = createMcpLogger('mcp-web-search', '1.0.0'); @@ -248,20 +243,90 @@ function createMcpServer() { .max(20) .default(5) .describe('Maximum number of results to return'), + safe_search: z + .enum(['strict', 'moderate', 'off']) + .default('moderate') + .describe('Safe search setting'), }, - async ({ query, limit = 5 }) => { - logger.info('Performing web search', { query, limit }); + async ({ query, limit = 5, safe_search = 'moderate' }) => { + const operationId = randomUUID(); - const searchResults = generateSearchResults(query, limit); + try { + logger.info('Performing DuckDuckGo web search', { + operationId, + query, + limit, + safe_search, + }); - return { - content: [ - { - type: 'text', - text: `Search Results for "${query}":\n\n${searchResults}`, - }, - ], - }; + // Map safe_search string to SafeSearchType enum + const safeSearchMap = { + strict: SafeSearchType.STRICT, + moderate: SafeSearchType.MODERATE, + off: SafeSearchType.OFF, + }; + + const searchOptions = { + safeSearch: safeSearchMap[safe_search], + time: null, // no time restriction + locale: 'en-us', + count: Math.min(limit, 20), // DuckDuckGo API limit + }; + + const searchResults = await search(query, searchOptions); + + if ( + !searchResults || + !searchResults.results || + searchResults.results.length === 0 + ) { + logger.warn('No search results found', { operationId, query }); + return { + content: [ + { + type: 'text', + text: `No search results found for "${query}".`, + }, + ], + }; + } + + const results = searchResults.results.slice(0, limit); + const formattedResults = results + .map((result, index) => { + return `${index + 1}. ${result.title}\n ${result.url}\n ${ + result.description + }\n`; + }) + .join('\n'); + + logger.info('DuckDuckGo search completed successfully', { + operationId, + query, + resultCount: results.length, + }); + + return { + content: [ + { + type: 'text', + text: `DuckDuckGo Search Results for "${query}":\n\n${formattedResults}`, + }, + ], + }; + } catch (error) { + logger.error('Failed to perform DuckDuckGo search', { + operationId, + query, + error: error.message, + stack: error.stack, + }); + + return createToolError( + new Error(`Failed to search DuckDuckGo: ${error.message}`), + { query, operationId } + ); + } } ); @@ -313,33 +378,6 @@ function createMcpServer() { return mcpServer; } -/** - * Generate simulated search results - */ -function generateSearchResults(query, limit) { - const results = []; - const domains = [ - 'example.com', - 'wikipedia.org', - 'github.com', - 'stackoverflow.com', - 'medium.com', - ]; - - for (let i = 0; i < Math.min(limit, 10); i++) { - const domain = domains[i % domains.length]; - const title = `${query} - Result ${i + 1}`; - const url = `https://${domain}/${query - .toLowerCase() - .replace(/\s+/g, '-')}-${i + 1}`; - const description = `This is a simulated search result for "${query}". It would normally contain relevant information about your search query.`; - - results.push(`${i + 1}. ${title}\n ${url}\n ${description}\n`); - } - - return results.join('\n'); -} - app.post('/mcp', async (req, res) => { try { logger.info('MCP POST request received', { @@ -454,7 +492,7 @@ app.listen(port, host, () => { logger.info(' GET /health - Health check'); logger.info('Available tools:'); logger.info(' - fetch_url - Fetch content from a URL'); - logger.info(' - search_web - Perform web search (simulated)'); + logger.info(' - search_web - Perform web search using DuckDuckGo'); logger.info(' - get_page_title - Extract title from a web page'); }); diff --git a/examples/mcp/mcp-servers/web-search/package-lock.json b/examples/mcp/mcp-servers/web-search/package-lock.json index 2741e26..bc62746 100644 --- a/examples/mcp/mcp-servers/web-search/package-lock.json +++ b/examples/mcp/mcp-servers/web-search/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.6.0", "cheerio": "^1.0.0-rc.12", "cors": "^2.8.5", + "duck-duck-scrape": "^2.2.7", "express": "^4.18.2", "winston": "^3.17.0", "zod": "^3.22.0" @@ -766,6 +767,19 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/duck-duck-scrape": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/duck-duck-scrape/-/duck-duck-scrape-2.2.7.tgz", + "integrity": "sha512-BEcglwnfx5puJl90KQfX+Q2q5vCguqyMpZcSRPBWk8OY55qWwV93+E+7DbIkrGDW4qkqPfUvtOUdi0lXz6lEMQ==", + "license": "MIT", + "dependencies": { + "html-entities": "^2.3.3", + "needle": "^3.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/Snazzah" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1160,6 +1174,22 @@ "node": ">= 0.4" } }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/htmlparser2": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", @@ -1362,6 +1392,22 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1680,6 +1726,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", diff --git a/examples/mcp/mcp-servers/web-search/package.json b/examples/mcp/mcp-servers/web-search/package.json index 0411055..818919a 100644 --- a/examples/mcp/mcp-servers/web-search/package.json +++ b/examples/mcp/mcp-servers/web-search/package.json @@ -13,6 +13,7 @@ "axios": "^1.6.0", "cheerio": "^1.0.0-rc.12", "cors": "^2.8.5", + "duck-duck-scrape": "^2.2.7", "express": "^4.18.2", "winston": "^3.17.0", "zod": "^3.22.0" From 3864adb1ade92319fc51e05ebd77b01f28addb08 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 19:30:28 +0000 Subject: [PATCH 25/38] refactor: Upgrade Node.js version in Dockerfiles and add MCP Inspector service Still need to figure out why I cannot connect to the other services using the inspector Signed-off-by: Eden Reich --- examples/mcp/README.md | 125 +++++++++++++++++++ examples/mcp/docker-compose.yml | 50 +++++++- examples/mcp/mcp-servers/memory/index.js | 7 +- examples/mcp/mcp-servers/web-search/index.js | 2 + 4 files changed, 177 insertions(+), 7 deletions(-) diff --git a/examples/mcp/README.md b/examples/mcp/README.md index 4cc1807..8130d10 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -50,3 +50,128 @@ All agents (Next.js, Vite, and Kubernetes) now include memory recovery capabilit - `clear-session`: Remove a saved session All agents will automatically use these tools when encountering HTTP errors, ensuring robust error recovery and task continuation. + +## 🔍 MCP Inspector + +The **MCP Inspector** is a visual debugging tool that allows you to inspect and test your MCP servers interactively. It provides a web-based interface to: + +- Connect to and inspect MCP servers +- View available tools and their schemas +- Test tool execution with custom parameters +- Debug server responses and error messages +- Monitor real-time server communication + +### Accessing the Inspector + +The MCP Inspector is available at: **http://localhost:6274** + +You can connect to any of the running MCP servers: + +- **Filesystem Server**: `http://mcp-filesystem:3000/mcp` +- **Web Search Server**: `http://mcp-web-search:3001/mcp` +- **Context7 Server**: `http://mcp-context7:3002/mcp` +- **NPM Server**: `http://mcp-npm:3003/mcp` +- **Memory Server**: `http://mcp-memory:3004/mcp` + +### Quick Inspector URLs + +For convenience, you can use these pre-configured URLs: + +```bash +# Connect to Filesystem server +http://localhost:6274/?transport=streamable-http&serverUrl=http://mcp-filesystem:3000/mcp + +# Connect to Web Search server +http://localhost:6274/?transport=streamable-http&serverUrl=http://mcp-web-search:3001/mcp + +# Connect to NPM server +http://localhost:6274/?transport=streamable-http&serverUrl=http://mcp-npm:3003/mcp +``` + +### Using the Inspector + +1. **Start the services**: `docker compose up --build` +2. **Open the inspector**: Navigate to http://localhost:6274 +3. **Connect to a server**: Enter an MCP server URL (e.g., `http://mcp-filesystem:3000/mcp`) +4. **Explore tools**: Browse available tools and their parameters +5. **Test execution**: Run tools with custom parameters and view results + +### Troubleshooting + +If you have to debug an MCP server, you can either use the MCP Inspector or standard curl commands. + +1. First go inside the a container that is attached to the same inference gateway network: + +```bash +docker run -it --rm alpine:latest sh -c "apk add --no-cache curl && sh" +``` + +2. For example let's check if mcp-web-search works as expected. We will get into the container: + +```sh +docker compose exec mcp-web-search sh + +export SERVER_URL="http://127.0.0.1" +export SERVER_PORT="" +export TOOL_NAME="" +``` + +3. Fetch a session ID by initializing the MCP server: + +```sh +SESSION_ID=$(curl -v -X POST "${SERVER_URL}:${SERVER_PORT}/mcp" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } + }' 2>&1 | grep -i "< mcp-session-id:" | cut -d' ' -f3 | tr -d '\r') +echo "mcp-session-id: $SESSION_ID" +``` + +4. List available tools to verify the server is running correctly: + +```sh +curl -v -X POST "${SERVER_URL}:${SERVER_PORT}/mcp" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + "params": {} + }' +``` + +5. Call a specific tool, for example, `search_web` to search the web: + +```sh +curl -X POST "${SERVER_URL}:${SERVER_PORT}/mcp" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "'"$TOOL_NAME"'", + "arguments": { + "sessionId": "'"$SESSION_ID"'", + "state": { + "query": "What is the capital of France?" + } + } + } + }' +``` diff --git a/examples/mcp/docker-compose.yml b/examples/mcp/docker-compose.yml index 74b4fb7..d4a583c 100644 --- a/examples/mcp/docker-compose.yml +++ b/examples/mcp/docker-compose.yml @@ -61,7 +61,7 @@ services: build: context: ./mcp-servers/filesystem dockerfile_inline: | - FROM node:18-alpine + FROM node:22-alpine WORKDIR /app RUN apk add --no-cache curl COPY package.json ./ @@ -90,18 +90,20 @@ services: build: context: ./mcp-servers/web-search dockerfile_inline: | - FROM node:18-alpine + FROM node:22-alpine WORKDIR /app RUN apk add --no-cache curl COPY package.json ./ RUN npm install COPY . . EXPOSE 3001 - CMD ["node", "index.js"] + CMD ["npm", "start"] environment: NODE_ENV: 'production' MCP_SERVER_NAME: 'web-search' MCP_SERVER_VERSION: '1.0.0' + PORT: '3001' + HOST: '0.0.0.0' networks: - inference-network healthcheck: @@ -116,7 +118,7 @@ services: build: context: ./mcp-servers/context7 dockerfile_inline: | - FROM node:18-alpine + FROM node:22-alpine WORKDIR /app RUN apk add --no-cache curl COPY package.json ./ @@ -176,7 +178,7 @@ services: build: context: ./mcp-servers/memory dockerfile_inline: | - FROM node:18-alpine + FROM node:22-alpine WORKDIR /app RUN apk add --no-cache curl COPY package.json ./ @@ -193,7 +195,7 @@ services: networks: - inference-network volumes: - - ./shared:/tmp/memory + - ./shared:/tmp healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:3004/health'] interval: 30s @@ -202,6 +204,42 @@ services: start_period: 45s restart: unless-stopped + mcp-inspector: + build: + context: . + dockerfile_inline: | + FROM node:22-alpine + WORKDIR /app + RUN apk add --no-cache curl git + RUN npm install -g @modelcontextprotocol/inspector + EXPOSE 6274 + CMD ["npx", "@modelcontextprotocol/inspector"] + environment: + NODE_ENV: 'production' + CLIENT_PORT: '6274' + ports: + - '6274:6274' + networks: + - inference-network + depends_on: + mcp-filesystem: + condition: service_healthy + mcp-web-search: + condition: service_healthy + mcp-context7: + condition: service_healthy + mcp-npm: + condition: service_healthy + mcp-memory: + condition: service_healthy + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:6274'] + interval: 30s + timeout: 10s + retries: 5 + start_period: 45s + restart: unless-stopped + # # Optional: Ollama for local models # ollama: # image: ollama/ollama:latest diff --git a/examples/mcp/mcp-servers/memory/index.js b/examples/mcp/mcp-servers/memory/index.js index 4d9d230..dd38d58 100644 --- a/examples/mcp/mcp-servers/memory/index.js +++ b/examples/mcp/mcp-servers/memory/index.js @@ -25,7 +25,6 @@ import { const app = express(); app.use(express.json()); -// Create standardized logger const logger = createMcpLogger('mcp-memory', '1.0.0'); const transports = {}; @@ -80,6 +79,12 @@ function createMcpServer() { lastError: null, }; + logger.info(`Saving state for session: ${sessionId}`, { + sessionId, + state: JSON.stringify(state), + context, + }); + const memoryPath = getMemoryPath(sessionId); await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); diff --git a/examples/mcp/mcp-servers/web-search/index.js b/examples/mcp/mcp-servers/web-search/index.js index adc5669..4dbb54c 100644 --- a/examples/mcp/mcp-servers/web-search/index.js +++ b/examples/mcp/mcp-servers/web-search/index.js @@ -15,10 +15,12 @@ import axios from 'axios'; import * as cheerio from 'cheerio'; import { search, SafeSearchType } from 'duck-duck-scrape'; import { createMcpLogger } from './logger.js'; +import cors from 'cors'; const logger = createMcpLogger('mcp-web-search', '1.0.0'); const app = express(); +app.use(cors()); app.use(express.json({ limit: '10mb' })); app.use((req, res, next) => { From bfa44122d4e0c8b879a298d441f92f1c27bc1a4e Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 19:42:11 +0000 Subject: [PATCH 26/38] refactor: Enhance logging and error handling in MCP Memory Server Signed-off-by: Eden Reich --- examples/mcp/mcp-servers/memory/index.js | 394 ++++++++++++++++++++++- 1 file changed, 377 insertions(+), 17 deletions(-) diff --git a/examples/mcp/mcp-servers/memory/index.js b/examples/mcp/mcp-servers/memory/index.js index dd38d58..ce65783 100644 --- a/examples/mcp/mcp-servers/memory/index.js +++ b/examples/mcp/mcp-servers/memory/index.js @@ -31,16 +31,28 @@ const transports = {}; const memoryDir = process.env.MEMORY_DIR || '/tmp/memory'; -console.info('Memory directory:', memoryDir); +logger.info('MCP Memory Server initializing', { + memoryDir, + nodeVersion: process.version, + platform: process.platform, + environment: process.env.NODE_ENV || 'development', +}); /** * Ensure memory directory exists */ async function ensureMemoryDir() { try { + logger.debug('Ensuring memory directory exists', { memoryDir }); await fs.mkdir(memoryDir, { recursive: true }); + logger.debug('Memory directory ready', { memoryDir }); } catch (error) { - console.error('Failed to create memory directory:', error); + logger.error('Failed to create memory directory', { + memoryDir, + error: error.message, + stack: error.stack, + }); + throw error; } } @@ -48,18 +60,24 @@ async function ensureMemoryDir() { * Get memory file path for a given session */ function getMemoryPath(sessionId) { - return path.join(memoryDir, `${sessionId}.json`); + const memoryPath = path.join(memoryDir, `${sessionId}.json`); + logger.debug('Generated memory path', { sessionId, memoryPath }); + return memoryPath; } /** * Create and configure the MCP server */ function createMcpServer() { + logger.debug('Creating new MCP server instance'); + const mcpServer = new McpServer({ name: 'memory', version: '1.0.0', }); + logger.debug('MCP server instance created'); + mcpServer.tool( 'save-state', { @@ -68,6 +86,15 @@ function createMcpServer() { context: z.string().optional().describe('Optional context description'), }, async ({ sessionId, state, context }) => { + const startTime = Date.now(); + + logger.info('save-state tool called', { + sessionId, + hasContext: !!context, + stateKeys: Object.keys(state || {}), + stateSize: JSON.stringify(state).length, + }); + try { await ensureMemoryDir(); @@ -79,6 +106,12 @@ function createMcpServer() { lastError: null, }; + logger.debug('Preparing memory data for save', { + sessionId, + dataSize: JSON.stringify(memoryData).length, + hasContext: !!context, + }); + logger.info(`Saving state for session: ${sessionId}`, { sessionId, state: JSON.stringify(state), @@ -88,6 +121,17 @@ function createMcpServer() { const memoryPath = getMemoryPath(sessionId); await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); + const duration = Date.now() - startTime; + + logger.info('State saved successfully', { + sessionId, + memoryPath, + duration, + dataSize: JSON.stringify(memoryData).length, + }); + + logMcpToolCall(logger, 'save-state', sessionId, { sessionId }); + return { content: [ { @@ -97,6 +141,18 @@ function createMcpServer() { ], }; } catch (error) { + const duration = Date.now() - startTime; + + logger.error('Failed to save state', { + sessionId, + error: error.message, + stack: error.stack, + duration, + }); + + logMcpError(logger, error, { sessionId }); + logMcpToolCall(logger, 'save-state', sessionId, { sessionId }); + return { content: [ { @@ -126,6 +182,18 @@ function createMcpServer() { context: z.string().optional().describe('Optional context description'), }, async ({ sessionId, state, error, context }) => { + const startTime = Date.now(); + + logger.info('save-error-state tool called', { + sessionId, + errorMessage: error.message, + errorCode: error.code, + errorStatus: error.status, + errorUrl: error.url, + hasContext: !!context, + stateKeys: Object.keys(state || {}), + }); + try { await ensureMemoryDir(); @@ -140,9 +208,29 @@ function createMcpServer() { }, }; + logger.debug('Preparing error memory data for save', { + sessionId, + dataSize: JSON.stringify(memoryData).length, + errorType: error.code || 'unknown', + }); + const memoryPath = getMemoryPath(sessionId); await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); + const duration = Date.now() - startTime; + + logger.info('Error state saved successfully', { + sessionId, + memoryPath, + duration, + errorMessage: error.message, + }); + + logMcpToolCall(logger, 'save-error-state', sessionId, { + sessionId, + error: error.message, + }); + return { content: [ { @@ -152,6 +240,22 @@ function createMcpServer() { ], }; } catch (saveError) { + const duration = Date.now() - startTime; + + logger.error('Failed to save error state', { + sessionId, + originalError: error.message, + saveError: saveError.message, + stack: saveError.stack, + duration, + }); + + logMcpError(logger, saveError, { + sessionId, + originalError: error.message, + }); + logMcpToolCall(logger, 'save-error-state', sessionId, { sessionId }); + return { content: [ { @@ -171,11 +275,34 @@ function createMcpServer() { sessionId: z.string().describe('Unique session identifier'), }, async ({ sessionId }) => { + const startTime = Date.now(); + + logger.info('restore-state tool called', { sessionId }); + try { const memoryPath = getMemoryPath(sessionId); + logger.debug('Attempting to read memory file', { + sessionId, + memoryPath, + }); + try { - const memoryData = JSON.parse(await fs.readFile(memoryPath, 'utf8')); + const fileContent = await fs.readFile(memoryPath, 'utf8'); + const memoryData = JSON.parse(fileContent); + + const duration = Date.now() - startTime; + + logger.info('State restored successfully', { + sessionId, + hasError: !!memoryData.lastError, + timestamp: memoryData.timestamp, + contextPresent: !!memoryData.context, + duration, + fileSize: fileContent.length, + }); + + logMcpToolCall(logger, 'restore-state', sessionId, { sessionId }); return { content: [ @@ -198,6 +325,14 @@ function createMcpServer() { }; } catch (readError) { if (readError.code === 'ENOENT') { + const duration = Date.now() - startTime; + + logger.info('No saved state found for session', { + sessionId, + duration, + }); + logMcpToolCall(logger, 'restore-state', sessionId, { sessionId }); + return { content: [ { @@ -207,9 +342,29 @@ function createMcpServer() { ], }; } + + logger.error('Failed to read memory file', { + sessionId, + memoryPath, + error: readError.message, + code: readError.code, + }); + throw readError; } } catch (error) { + const duration = Date.now() - startTime; + + logger.error('Failed to restore state', { + sessionId, + error: error.message, + stack: error.stack, + duration, + }); + + logMcpError('restore-state', error, { sessionId }); + logMcpToolCall('restore-state', { sessionId }, false, duration); + return { content: [ { @@ -224,18 +379,30 @@ function createMcpServer() { ); mcpServer.tool('list-sessions', {}, async () => { + const startTime = Date.now(); + + logger.info('list-sessions tool called'); + try { await ensureMemoryDir(); const files = await fs.readdir(memoryDir); const jsonFiles = files.filter((file) => file.endsWith('.json')); + logger.debug('Found session files', { + totalFiles: files.length, + jsonFiles: jsonFiles.length, + files: jsonFiles, + }); + const sessions = []; + let successCount = 0; + let errorCount = 0; + for (const file of jsonFiles) { try { const sessionId = path.basename(file, '.json'); - const memoryData = JSON.parse( - await fs.readFile(path.join(memoryDir, file), 'utf8') - ); + const filePath = path.join(memoryDir, file); + const memoryData = JSON.parse(await fs.readFile(filePath, 'utf8')); sessions.push({ sessionId, @@ -244,14 +411,35 @@ function createMcpServer() { hasError: !!memoryData.lastError, lastError: memoryData.lastError?.message, }); + + successCount++; + + logger.debug('Session file processed successfully', { + sessionId, + file, + hasError: !!memoryData.lastError, + }); } catch (readError) { - console.warn( - `Failed to read session file ${file}:`, - readError.message - ); + errorCount++; + + logger.warn('Failed to read session file', { + file, + error: readError.message, + }); } } + const duration = Date.now() - startTime; + + logger.info('Sessions listed successfully', { + totalSessions: sessions.length, + successCount, + errorCount, + duration, + }); + + logMcpToolCall('list-sessions', {}, true, duration); + return { content: [ { @@ -261,6 +449,17 @@ function createMcpServer() { ], }; } catch (error) { + const duration = Date.now() - startTime; + + logger.error('Failed to list sessions', { + error: error.message, + stack: error.stack, + duration, + }); + + logMcpError('list-sessions', error, {}); + logMcpToolCall('list-sessions', {}, false, duration); + return { content: [ { @@ -279,10 +478,25 @@ function createMcpServer() { sessionId: z.string().describe('Unique session identifier'), }, async ({ sessionId }) => { + const startTime = Date.now(); + + logger.info('clear-session tool called', { sessionId }); + try { const memoryPath = getMemoryPath(sessionId); + + logger.debug('Attempting to delete session file', { + sessionId, + memoryPath, + }); + await fs.unlink(memoryPath); + const duration = Date.now() - startTime; + + logger.info('Session cleared successfully', { sessionId, duration }); + logMcpToolCall('clear-session', { sessionId }, true, duration); + return { content: [ { @@ -292,7 +506,12 @@ function createMcpServer() { ], }; } catch (error) { + const duration = Date.now() - startTime; + if (error.code === 'ENOENT') { + logger.info('No session found to clear', { sessionId, duration }); + logMcpToolCall('clear-session', { sessionId }, true, duration); + return { content: [ { @@ -303,6 +522,17 @@ function createMcpServer() { }; } + logger.error('Failed to clear session', { + sessionId, + error: error.message, + code: error.code, + stack: error.stack, + duration, + }); + + logMcpError('clear-session', error, { sessionId }); + logMcpToolCall('clear-session', { sessionId }, false, duration); + return { content: [ { @@ -316,6 +546,7 @@ function createMcpServer() { } ); + logger.debug('All MCP tools registered'); return mcpServer; } @@ -323,12 +554,35 @@ function createMcpServer() { * Handle MCP requests */ app.post('/mcp', async (req, res) => { + const requestId = randomUUID(); const sessionId = req.headers['mcp-session-id'] || randomUUID(); + const startTime = Date.now(); + + logger.info('MCP request received', { + requestId, + sessionId, + method: req.body?.method, + hasParams: !!req.body?.params, + userAgent: req.headers['user-agent'], + contentType: req.headers['content-type'], + contentLength: req.headers['content-length'], + }); + + logMcpRequest(logger, req, { + sessionId, + requestId, + params: req.body?.params, + }); try { let transport = transports[sessionId]; if (!transport) { + logger.debug('Creating new transport for session', { + sessionId, + requestId, + }); + transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => sessionId, }); @@ -336,16 +590,53 @@ app.post('/mcp', async (req, res) => { transports[sessionId] = transport; transport.onclose = () => { + logger.debug('Transport closed for session', { sessionId }); delete transports[sessionId]; }; const mcpServer = createMcpServer(); await mcpServer.connect(transport); + + logger.debug('MCP server connected to transport', { + sessionId, + requestId, + }); + logMcpSession(logger, 'connected', sessionId); + } else { + logger.debug('Reusing existing transport for session', { + sessionId, + requestId, + }); } await transport.handleRequest(req, res, req.body); + + const duration = Date.now() - startTime; + + logger.info('MCP request handled successfully', { + requestId, + sessionId, + duration, + method: req.body?.method, + }); } catch (error) { - console.error('Error handling MCP request:', error); + const duration = Date.now() - startTime; + + logger.error('Error handling MCP request', { + requestId, + sessionId, + error: error.message, + stack: error.stack, + method: req.body?.method, + duration, + }); + + logMcpError(logger, error, { + sessionId, + requestId, + method: req.body?.method, + }); + if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', @@ -359,13 +650,34 @@ app.post('/mcp', async (req, res) => { } }); -// Health check endpoint app.get('/health', (req, res) => { - res.status(200).json({ status: 'healthy' }); + const requestId = randomUUID(); + + logger.debug('Health check requested', { + requestId, + userAgent: req.headers['user-agent'], + }); + + const healthData = { + status: 'healthy', + timestamp: new Date().toISOString(), + activeSessions: Object.keys(transports).length, + memoryDir, + }; + + logger.debug('Health check response', { requestId, healthData }); + + res.status(200).json(healthData); }); -// Method not allowed handlers app.get('/mcp', (req, res) => { + const requestId = randomUUID(); + + logger.warn('Method not allowed: GET /mcp', { + requestId, + userAgent: req.headers['user-agent'], + }); + res.status(405).json({ jsonrpc: '2.0', error: { @@ -377,6 +689,13 @@ app.get('/mcp', (req, res) => { }); app.delete('/mcp', (req, res) => { + const requestId = randomUUID(); + + logger.warn('Method not allowed: DELETE /mcp', { + requestId, + userAgent: req.headers['user-agent'], + }); + res.status(405).json({ jsonrpc: '2.0', error: { @@ -387,8 +706,49 @@ app.delete('/mcp', (req, res) => { }); }); -// Start the server const PORT = process.env.PORT || 3004; + +logger.info('Starting MCP Memory Server', { + port: PORT, + memoryDir, + nodeVersion: process.version, + environment: process.env.NODE_ENV || 'development', +}); + app.listen(PORT, () => { - console.log(`MCP Memory Server listening on port ${PORT}`); + logger.info('MCP Memory Server started successfully', { + port: PORT, + memoryDir, + endpoints: ['/mcp', '/health'], + }); +}); + +process.on('SIGTERM', () => { + logger.info('SIGTERM received, shutting down gracefully'); + + Object.keys(transports).forEach((sessionId) => { + logger.debug('Closing transport for session', { sessionId }); + const transport = transports[sessionId]; + if (transport && transport.close) { + transport.close(); + } + }); + + logger.info('MCP Memory Server shutdown complete'); + process.exit(0); +}); + +process.on('SIGINT', () => { + logger.info('SIGINT received, shutting down gracefully'); + + Object.keys(transports).forEach((sessionId) => { + logger.debug('Closing transport for session', { sessionId }); + const transport = transports[sessionId]; + if (transport && transport.close) { + transport.close(); + } + }); + + logger.info('MCP Memory Server shutdown complete'); + process.exit(0); }); From e8253e45aa208204773ff2fcd833efed7f175454 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 19:44:20 +0000 Subject: [PATCH 27/38] refactor: Clean up unneeded comments Signed-off-by: Eden Reich --- examples/mcp/mcp-servers/npm/index.js | 30 --------------------------- 1 file changed, 30 deletions(-) diff --git a/examples/mcp/mcp-servers/npm/index.js b/examples/mcp/mcp-servers/npm/index.js index c231d31..035f0cf 100644 --- a/examples/mcp/mcp-servers/npm/index.js +++ b/examples/mcp/mcp-servers/npm/index.js @@ -26,20 +26,15 @@ import { const execAsync = promisify(exec); -// Create standardized logger const logger = createMcpLogger('mcp-npm', '1.0.0'); -// Express app for HTTP transport const app = express(); app.use(express.json()); -// Map to store transports by session ID const transports = {}; -// Working directory (configurable via environment) const workingDirectory = process.env.WORKING_DIRECTORY || '/tmp'; -// Whitelisted npm commands for security const ALLOWED_NPM_COMMANDS = [ 'init', 'install', @@ -73,7 +68,6 @@ function validateNpmCommand(command) { throw new Error('Empty command'); } - // Remove 'npm' if it's the first part if (parts[0] === 'npm') { parts.shift(); } @@ -144,7 +138,6 @@ function createMcpServer() { version: '1.0.0', }); - // Tool: Run npm command mcpServer.tool( 'npm_run', { @@ -207,7 +200,6 @@ function createMcpServer() { } ); - // Tool: Initialize new npm project mcpServer.tool( 'npm_init', { @@ -229,10 +221,8 @@ function createMcpServer() { }); try { - // Create project directory await execAsync(`mkdir -p "${projectDir}"`); - // Initialize npm project const initCommand = yes ? 'init -y' : 'init'; const result = await executeNpmCommand(initCommand, projectDir); @@ -276,7 +266,6 @@ function createMcpServer() { } ); - // Tool: Install npm packages mcpServer.tool( 'npm_install', { @@ -362,7 +351,6 @@ function createMcpServer() { } ); - // Tool: Create Next.js project mcpServer.tool( 'create_nextjs_project', { @@ -417,10 +405,8 @@ function createMcpServer() { }); try { - // Build the npx create-next-app command with options let command = `npx create-next-app@latest "${name}" --yes`; - // Add flags based on options if (typescript) { command += ' --typescript'; } else { @@ -533,12 +519,10 @@ function createMcpServer() { * Setup MCP endpoints for proper Model Context Protocol communication */ function setupSessionRoutes() { - // Handle POST requests for MCP communication app.post('/mcp', async (req, res) => { try { logMcpRequest(logger, req); - // Fix missing Accept headers for compatibility with Go MCP clients const accept = req.headers.accept || req.headers.Accept; if ( !accept || @@ -549,25 +533,20 @@ function setupSessionRoutes() { req.headers.accept = 'application/json, text/event-stream'; } - // Check for existing session ID const sessionId = req.headers['mcp-session-id']; let transport; if (sessionId && transports[sessionId]) { - // Reuse existing transport transport = transports[sessionId]; } else { - // Create new transport for new session transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (newSessionId) => { logMcpSession(logger, 'initialized', newSessionId); - // Store the transport by session ID transports[newSessionId] = transport; }, }); - // Clean up transport when closed transport.onclose = () => { if (transport.sessionId) { logMcpSession(logger, 'closed', transport.sessionId); @@ -575,12 +554,10 @@ function setupSessionRoutes() { } }; - // Create and connect MCP server const server = createMcpServer(); await server.connect(transport); } - // Handle the MCP request await transport.handleRequest(req, res, req.body); } catch (error) { logMcpError(logger, error, { @@ -600,7 +577,6 @@ function setupSessionRoutes() { } }); - // Handle GET requests for SSE (server-to-client notifications) app.get('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !transports[sessionId]) { @@ -612,7 +588,6 @@ function setupSessionRoutes() { await transport.handleRequest(req, res); }); - // Handle DELETE requests for session termination app.delete('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id']; if (!sessionId || !transports[sessionId]) { @@ -647,7 +622,6 @@ function setupHealthCheck() { async function startServer() { const port = process.env.PORT || 3003; - // Setup routes setupSessionRoutes(); setupHealthCheck(); @@ -673,10 +647,8 @@ async function startServer() { }); } -// Graceful shutdown process.on('SIGTERM', () => { logger.info('Received SIGTERM, shutting down gracefully'); - // Close all transports Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); }); @@ -685,14 +657,12 @@ process.on('SIGTERM', () => { process.on('SIGINT', () => { logger.info('Received SIGINT, shutting down gracefully'); - // Close all transports Object.values(transports).forEach((transport) => { if (transport.close) transport.close(); }); process.exit(0); }); -// Start the server startServer().catch((error) => { logMcpError(logger, error, { operation: 'server-startup', From 1b46a15fe18550dffe5d0a8e2527d4c28997fd95 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 19:45:17 +0000 Subject: [PATCH 28/38] refactor: Remove redundant comments and improve code clarity in MCP Filesystem Server Signed-off-by: Eden Reich --- examples/mcp/mcp-servers/filesystem/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/examples/mcp/mcp-servers/filesystem/index.js b/examples/mcp/mcp-servers/filesystem/index.js index 641c466..7c3720a 100644 --- a/examples/mcp/mcp-servers/filesystem/index.js +++ b/examples/mcp/mcp-servers/filesystem/index.js @@ -21,17 +21,13 @@ import { logMcpError, } from './logger.js'; -// Express app for HTTP transport const app = express(); app.use(express.json()); -// Create standardized logger const logger = createMcpLogger('mcp-filesystem', '1.0.0'); -// Map to store transports by session ID const transports = {}; -// Allowed directories (configurable via environment) const allowedDirectories = (process.env.ALLOWED_DIRECTORIES || '/tmp').split( ',' ); @@ -487,7 +483,6 @@ function createMcpServer() { * Setup MCP endpoints for proper Model Context Protocol communication */ function setupSessionRoutes() { - // Handle POST requests for MCP communication app.post('/mcp', async (req, res) => { try { logMcpRequest(logger, req, 'MCP POST request received'); From 4bd68d075e84dd2ff4d3b7d15edcc3fc5bff812d Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 19:46:04 +0000 Subject: [PATCH 29/38] refactor: Remove redundant comment in Context7Process class Signed-off-by: Eden Reich --- examples/mcp/mcp-servers/context7/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/mcp/mcp-servers/context7/index.js b/examples/mcp/mcp-servers/context7/index.js index 5c3d840..53603f7 100644 --- a/examples/mcp/mcp-servers/context7/index.js +++ b/examples/mcp/mcp-servers/context7/index.js @@ -79,7 +79,6 @@ class Context7Process { } }); - // Handle stderr - logs and errors this.process.stderr.on('data', (data) => { const message = data.toString().trim(); if (message.includes('ready') || message.includes('listening')) { From 1222d2ee62c78573babfedd2cc4c07a7ea6a992d Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 21:39:17 +0000 Subject: [PATCH 30/38] refactor: Update @inference-gateway/sdk version to ^0.7.3 and improve model configuration in multiple agents Signed-off-by: Eden Reich --- examples/mcp/agents/kubernetes/index.ts | 2 +- .../mcp/agents/kubernetes/package-lock.json | 8 +- examples/mcp/agents/kubernetes/package.json | 2 +- examples/mcp/agents/nextjs/index.ts | 130 +++++++++++++++--- examples/mcp/agents/nextjs/package-lock.json | 8 +- examples/mcp/agents/nextjs/package.json | 2 +- examples/mcp/agents/vite/index.ts | 2 +- examples/mcp/agents/vite/package-lock.json | 8 +- examples/mcp/agents/vite/package.json | 2 +- examples/mcp/mcp-servers/web-search/index.js | 72 ++++++++-- 10 files changed, 194 insertions(+), 42 deletions(-) diff --git a/examples/mcp/agents/kubernetes/index.ts b/examples/mcp/agents/kubernetes/index.ts index 596853e..9ab30bb 100644 --- a/examples/mcp/agents/kubernetes/index.ts +++ b/examples/mcp/agents/kubernetes/index.ts @@ -472,7 +472,7 @@ If Kubernetes configurations exist: await this.config.client.streamChatCompletion( { - model: `${this.config.provider}/${this.config.model}`, + model: this.config.model, messages: this.config.conversationHistory, max_tokens: 2000, }, diff --git a/examples/mcp/agents/kubernetes/package-lock.json b/examples/mcp/agents/kubernetes/package-lock.json index f264dac..8226e08 100644 --- a/examples/mcp/agents/kubernetes/package-lock.json +++ b/examples/mcp/agents/kubernetes/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.2", + "@inference-gateway/sdk": "^0.7.3", "axios": "^1.9.0", "dotenv": "^16.5.0" }, @@ -443,9 +443,9 @@ } }, "node_modules/@inference-gateway/sdk": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.2.tgz", - "integrity": "sha512-oQRsGeY0L71C+H1JIyskslpenfS9GbCGvNBeSa5Twga/kgxG+iKGEb9+F7+Jc59ZyrHdMS5bRf3StRRZ4qci6Q==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.3.tgz", + "integrity": "sha512-UCbapsrblks9A0armTyXIdBM1NHy+8blZNkSP4AeTlHqH8CpVHH2gcgNRyn2SgqewF61kPXkijj0fh1XLZM7OA==", "license": "MIT", "engines": { "node": ">=22.12.0", diff --git a/examples/mcp/agents/kubernetes/package.json b/examples/mcp/agents/kubernetes/package.json index 293a4fa..38612e4 100644 --- a/examples/mcp/agents/kubernetes/package.json +++ b/examples/mcp/agents/kubernetes/package.json @@ -17,7 +17,7 @@ "author": "", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.2", + "@inference-gateway/sdk": "^0.7.3", "axios": "^1.9.0", "dotenv": "^16.5.0" }, diff --git a/examples/mcp/agents/nextjs/index.ts b/examples/mcp/agents/nextjs/index.ts index 8623dd1..df3fc20 100644 --- a/examples/mcp/agents/nextjs/index.ts +++ b/examples/mcp/agents/nextjs/index.ts @@ -10,9 +10,11 @@ import { MessageRole, Provider, } from '@inference-gateway/sdk'; +import { randomUUID } from 'crypto'; import * as dotenv from 'dotenv'; import * as path from 'path'; import * as readline from 'readline'; +import { clearTimeout, setTimeout } from 'timers'; dotenv.config({ path: path.join(__dirname, '.env') }); @@ -32,6 +34,7 @@ interface AgentConfig { maxHistoryLength: number; sessionId: string; memoryEnabled: boolean; + abortController: globalThis.AbortController; } class NextJSAgent { @@ -39,6 +42,10 @@ class NextJSAgent { private rl: readline.Interface; constructor() { + console.log('🔧 Debug - Environment variables:'); + console.log(' PROVIDER:', process.env.PROVIDER); + console.log(' LLM:', process.env.LLM); + this.config = { client: new InferenceGatewayClient({ baseURL: 'http://inference-gateway:8080/v1', @@ -52,8 +59,9 @@ class NextJSAgent { totalTokensUsed: 0, maxTokensPerRequest: 1000, maxHistoryLength: 10, - sessionId: `nextjs-agent-${Date.now()}`, + sessionId: process.env.SESSION_ID || randomUUID(), memoryEnabled: true, + abortController: new globalThis.AbortController(), }; this.rl = readline.createInterface({ @@ -68,8 +76,24 @@ class NextJSAgent { } private async delay(ms: number): Promise { - return new Promise((resolve) => { - global.setTimeout(() => resolve(), ms); + return new Promise((resolve, reject) => { + if (this.config.abortController.signal.aborted) { + reject(new Error('Aborted')); + return; + } + + const timeout = global.setTimeout(() => resolve(), ms); + + const abortHandler = () => { + global.clearTimeout(timeout); + reject(new Error('Aborted')); + }; + + this.config.abortController.signal.addEventListener( + 'abort', + abortHandler, + { once: true } + ); }); } @@ -85,6 +109,14 @@ class NextJSAgent { return ` You are an expert software development assistant with access to Context7 MCP tools for library documentation and research. Today is **June 1, 2025**. +**ABSOLUTELY CRITICAL - READ THIS FIRST**: +- You must NEVER output XML tags or function calls in any format +- You must NEVER use syntax like , , or +- Tools are handled automatically by the MCP system - you just describe what you need +- When you want to search: Say "I need to search for X" - don't output XML +- When you want to fetch: Say "I need to get information from Y" - don't output XML +- Just communicate naturally and the system will handle all tool calling + --- ### 🔧 CORE RESPONSIBILITIES @@ -108,6 +140,12 @@ You have access to several MCP tool categories: * c41_resolve-library-id: Resolve technology names to Context7-compatible IDs * c41_get-library-docs: Fetch full documentation, usage examples, and best practices +**Web Search Tools:** + +* search_web: Perform web searches using DuckDuckGo (use this FIRST before fetching URLs) +* fetch_url: Fetch content from verified URLs (only use URLs from search results) +* get_page_title: Extract page titles from URLs + **Mock Tools (for local/demo use):** * search_libraries: Search for libraries by name or functionality @@ -138,7 +176,13 @@ You have access to several MCP tool categories: * npm_install: Install npm packages * create_nextjs_project: Create a new Next.js project with specified options -**IMPORTANT**: All tools are called through the MCP system - never use XML-style syntax like . The LLM will automatically use the available tools when needed. +**CRITICAL TOOL USAGE RULES**: +- NEVER use XML-style syntax like or tags +- NEVER output function calls in XML format like +- Tools are automatically available and will be called by the system when you need them +- Simply describe what you want to do and the system will handle tool calls +- If you need to search, just say "I need to search for..." and the system will call search_web +- If you need to fetch a URL, just say "I need to fetch..." and the system will call fetch_url --- @@ -172,10 +216,16 @@ When encountering HTTP errors or failures: **When creating a Next.js project, always wait 30 seconds after project creation.** -**CRITICAL: Never use XML-style tool syntax like \`\`. All tools are automatically available through MCP and will be called by the LLM when needed.** +**CRITICAL: Never use XML-style tool syntax like \`\` or \`\` tags. All tools are automatically available through MCP and will be called by the LLM when needed. Simply describe what you want to do in natural language.** + +**Web Search Best Practices:** +1. **Always search first**: Use search_web to find information before trying to fetch URLs +2. **Use reliable URLs**: Only fetch URLs from search results or known reliable domains +3. **Verify domains**: Stick to major sites like github.com, stackoverflow.com, docs sites, etc. +4. **Search workflow**: search_web → get reliable URLs → fetch_url with those URLs 1. Clarify requirements and tech stack -2. Lookup technologies using Context7 tools +2. Lookup technologies using Context7 tools OR web search tools 3. Retrieve current documentation and patterns 4. Scaffold or enhance projects under /tmp, maintaining clean structure 5. Follow framework and language conventions @@ -492,12 +542,18 @@ If a Next.js project exists: ); } + this.resetAbortController(); + let assistantResponse = ''; let shouldWaitForProject = false; + console.log( + `🔧 Debug - Using provider: ${this.config.provider}, model: ${this.config.model}` + ); + await this.config.client.streamChatCompletion( { - model: `${this.config.provider}/${this.config.model}`, + model: this.config.model, messages: this.getOptimizedConversationHistory(), max_tokens: this.config.maxTokensPerRequest, }, @@ -575,7 +631,6 @@ If a Next.js project exists: onError: (error) => { console.error(`\n❌ Stream Error: ${error.error}`); - // Save error state to memory for recovery if (this.config.memoryEnabled) { this.saveStateToMemory( `Error occurred during request processing: ${error.error}` @@ -599,7 +654,9 @@ If a Next.js project exists: }); } }, - } + }, + this.config.provider, + this.config.abortController.signal ); } @@ -626,7 +683,7 @@ If a Next.js project exists: await this.config.client.streamChatCompletion( { - model: `${this.config.provider}/${this.config.model}`, + model: this.config.model, messages: [ { role: MessageRole.system, @@ -675,7 +732,9 @@ Call save-state tool immediately with sessionId="${this.config.sessionId}" and t console.warn('⚠️ Memory tool called but save may have failed'); } }, - } + }, + this.config.provider, + this.config.abortController.signal ); } catch (error) { console.warn( @@ -708,12 +767,17 @@ Call save-state tool immediately with sessionId="${this.config.sessionId}" and t const maxAttempts = 3; for (let attempt = 1; attempt <= maxAttempts; attempt++) { + if (this.config.abortController.signal.aborted) { + console.warn('⚠️ Memory save aborted during shutdown'); + throw new Error('Memory save aborted'); + } + console.log(`🔄 Memory save attempt ${attempt}/${maxAttempts}`); try { await this.config.client.streamChatCompletion( { - model: `${this.config.provider}/${this.config.model}`, + model: this.config.model, messages: [ { role: MessageRole.system, @@ -751,7 +815,9 @@ Call the save-state tool now.`, JSON.stringify(args, null, 2) ); } catch { - // Ignore parsing errors + console.error( + `📝 Raw tool arguments: ${toolCall.function.arguments}` + ); } } }, @@ -779,7 +845,9 @@ Call the save-state tool now.`, ); } }, - } + }, + this.config.provider, + this.config.abortController.signal ); if (toolCallDetected && saveSuccessful) { @@ -787,6 +855,10 @@ Call the save-state tool now.`, } if (attempt < maxAttempts) { + if (this.config.abortController.signal.aborted) { + console.warn('⚠️ Memory save aborted during retry wait'); + throw new Error('Memory save aborted'); + } console.log(`⏳ Waiting 2 seconds before retry...`); await this.delay(2000); } @@ -829,7 +901,7 @@ Call the save-state tool now.`, await this.config.client.streamChatCompletion( { - model: `${this.config.provider}/${this.config.model}`, + model: this.config.model, messages: [ { role: MessageRole.system, @@ -846,7 +918,7 @@ Call the save-state tool now.`, onContent: (content) => { if (content.includes('{') && content.includes('}')) { try { - const jsonMatch = content.match(/\{.*\}/s); + const jsonMatch = content.match(/\{[\s\S]*\}/); if (jsonMatch) { restoredData = JSON.parse(jsonMatch[0]); } @@ -881,7 +953,9 @@ Call the save-state tool now.`, ); } }, - } + }, + this.config.provider, + this.config.abortController.signal ); return !!restoredData; @@ -958,13 +1032,27 @@ Call the save-state tool now.`, return this.config.conversationHistory; } + private resetAbortController(): void { + if (!this.config.abortController.signal.aborted) { + this.config.abortController.abort('Starting new request'); + } + this.config.abortController = new globalThis.AbortController(); + } + async shutdown(): Promise { if (this.config.memoryEnabled) { console.log('💾 Saving session state before shutdown...'); try { + const shutdownTimeout = setTimeout(() => { + console.warn('⚠️ Shutdown timeout reached, forcing exit...'); + process.exit(1); + }, 10000); // 10 second timeout + await this.saveStateToMemoryForced( 'Manual shutdown via SIGINT/SIGTERM signal' ); + + clearTimeout(shutdownTimeout); console.log('✅ Session state saved successfully'); } catch (error) { console.warn( @@ -976,6 +1064,12 @@ Call the save-state tool now.`, this.rl.close(); } + + abortOperations(): void { + if (!this.config.abortController.signal.aborted) { + this.config.abortController.abort('Shutdown signal received'); + } + } } async function runNextJSAgent(): Promise { @@ -983,12 +1077,14 @@ async function runNextJSAgent(): Promise { process.on('SIGINT', async () => { console.log('\n\n👋 Shutting down NextJS Agent...'); + agent.abortOperations(); await agent.shutdown(); process.exit(0); }); process.on('SIGTERM', async () => { console.log('\n\n👋 Shutting down NextJS Agent...'); + agent.abortOperations(); await agent.shutdown(); process.exit(0); }); diff --git a/examples/mcp/agents/nextjs/package-lock.json b/examples/mcp/agents/nextjs/package-lock.json index f264dac..8226e08 100644 --- a/examples/mcp/agents/nextjs/package-lock.json +++ b/examples/mcp/agents/nextjs/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.2", + "@inference-gateway/sdk": "^0.7.3", "axios": "^1.9.0", "dotenv": "^16.5.0" }, @@ -443,9 +443,9 @@ } }, "node_modules/@inference-gateway/sdk": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.2.tgz", - "integrity": "sha512-oQRsGeY0L71C+H1JIyskslpenfS9GbCGvNBeSa5Twga/kgxG+iKGEb9+F7+Jc59ZyrHdMS5bRf3StRRZ4qci6Q==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.3.tgz", + "integrity": "sha512-UCbapsrblks9A0armTyXIdBM1NHy+8blZNkSP4AeTlHqH8CpVHH2gcgNRyn2SgqewF61kPXkijj0fh1XLZM7OA==", "license": "MIT", "engines": { "node": ">=22.12.0", diff --git a/examples/mcp/agents/nextjs/package.json b/examples/mcp/agents/nextjs/package.json index 245d1f7..cd8dcdf 100644 --- a/examples/mcp/agents/nextjs/package.json +++ b/examples/mcp/agents/nextjs/package.json @@ -11,7 +11,7 @@ "author": "", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.2", + "@inference-gateway/sdk": "^0.7.3", "axios": "^1.9.0", "dotenv": "^16.5.0" }, diff --git a/examples/mcp/agents/vite/index.ts b/examples/mcp/agents/vite/index.ts index a5060d3..eafe31f 100644 --- a/examples/mcp/agents/vite/index.ts +++ b/examples/mcp/agents/vite/index.ts @@ -453,7 +453,7 @@ If a Vite project exists: await this.config.client.streamChatCompletion( { - model: `${this.config.provider}/${this.config.model}`, + model: this.config.model, messages: this.config.conversationHistory, max_tokens: 2000, }, diff --git a/examples/mcp/agents/vite/package-lock.json b/examples/mcp/agents/vite/package-lock.json index f264dac..8226e08 100644 --- a/examples/mcp/agents/vite/package-lock.json +++ b/examples/mcp/agents/vite/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.2", + "@inference-gateway/sdk": "^0.7.3", "axios": "^1.9.0", "dotenv": "^16.5.0" }, @@ -443,9 +443,9 @@ } }, "node_modules/@inference-gateway/sdk": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.2.tgz", - "integrity": "sha512-oQRsGeY0L71C+H1JIyskslpenfS9GbCGvNBeSa5Twga/kgxG+iKGEb9+F7+Jc59ZyrHdMS5bRf3StRRZ4qci6Q==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.3.tgz", + "integrity": "sha512-UCbapsrblks9A0armTyXIdBM1NHy+8blZNkSP4AeTlHqH8CpVHH2gcgNRyn2SgqewF61kPXkijj0fh1XLZM7OA==", "license": "MIT", "engines": { "node": ">=22.12.0", diff --git a/examples/mcp/agents/vite/package.json b/examples/mcp/agents/vite/package.json index 293a4fa..38612e4 100644 --- a/examples/mcp/agents/vite/package.json +++ b/examples/mcp/agents/vite/package.json @@ -17,7 +17,7 @@ "author": "", "license": "ISC", "dependencies": { - "@inference-gateway/sdk": "^0.7.2", + "@inference-gateway/sdk": "^0.7.3", "axios": "^1.9.0", "dotenv": "^16.5.0" }, diff --git a/examples/mcp/mcp-servers/web-search/index.js b/examples/mcp/mcp-servers/web-search/index.js index 4dbb54c..9fa8451 100644 --- a/examples/mcp/mcp-servers/web-search/index.js +++ b/examples/mcp/mcp-servers/web-search/index.js @@ -10,6 +10,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import express from 'express'; import { randomUUID } from 'node:crypto'; +import { setTimeout } from 'node:timers/promises'; import { z } from 'zod'; import axios from 'axios'; import * as cheerio from 'cheerio'; @@ -19,6 +20,47 @@ import cors from 'cors'; const logger = createMcpLogger('mcp-web-search', '1.0.0'); +let lastSearchTime = 0; +const MIN_SEARCH_INTERVAL = 2000; + +const rateLimitedSearch = async (query, options, retries = 3) => { + const now = Date.now(); + const timeSinceLastSearch = now - lastSearchTime; + + if (timeSinceLastSearch < MIN_SEARCH_INTERVAL) { + const delay = MIN_SEARCH_INTERVAL - timeSinceLastSearch; + logger.info('Rate limiting: delaying search', { delay, query }); + await setTimeout(delay); + } + + lastSearchTime = Date.now(); + + for (let attempt = 1; attempt <= retries; attempt++) { + try { + return await search(query, options); + } catch (error) { + if ( + error.message.includes('anomaly') || + error.message.includes('too quickly') + ) { + if (attempt < retries) { + const backoffDelay = MIN_SEARCH_INTERVAL * attempt; + logger.warn('DuckDuckGo rate limit hit, retrying', { + attempt, + retries, + delay: backoffDelay, + query, + error: error.message, + }); + await setTimeout(backoffDelay); + continue; + } + } + throw error; + } + } +}; + const app = express(); app.use(cors()); app.use(express.json({ limit: '10mb' })); @@ -270,12 +312,12 @@ function createMcpServer() { const searchOptions = { safeSearch: safeSearchMap[safe_search], - time: null, // no time restriction + time: null, locale: 'en-us', - count: Math.min(limit, 20), // DuckDuckGo API limit + count: Math.min(limit, 20), }; - const searchResults = await search(query, searchOptions); + const searchResults = await rateLimitedSearch(query, searchOptions); if ( !searchResults || @@ -385,6 +427,7 @@ app.post('/mcp', async (req, res) => { logger.info('MCP POST request received', { headers: req.headers, bodyKeys: Object.keys(req.body || {}), + method: req.body?.method, }); const accept = req.headers.accept || req.headers.Accept; @@ -402,12 +445,16 @@ app.post('/mcp', async (req, res) => { if (sessionId && transports[sessionId]) { transport = transports[sessionId]; + logger.info('Using existing session', { sessionId }); } else { + const newSessionId = randomUUID(); + logger.info('Creating new MCP session', { sessionId: newSessionId }); + transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - onsessioninitialized: (newSessionId) => { - logger.info('MCP session initialized', { sessionId: newSessionId }); - transports[newSessionId] = transport; + sessionIdGenerator: () => newSessionId, + onsessioninitialized: (initSessionId) => { + logger.info('MCP session initialized', { sessionId: initSessionId }); + transports[initSessionId] = transport; }, }); @@ -420,11 +467,20 @@ app.post('/mcp', async (req, res) => { const server = createMcpServer(); await server.connect(transport); + + transports[newSessionId] = transport; + + res.setHeader('mcp-session-id', newSessionId); } await transport.handleRequest(req, res, req.body); } catch (error) { - logger.error('Error handling MCP request', { error: error.message }); + logger.error('Error handling MCP request', { + error: error.message, + stack: error.stack, + method: req.body?.method, + sessionId: req.headers['mcp-session-id'], + }); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', From 39d7cf6dfd2b3424748280c6a6350110c0add3fe Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Sun, 1 Jun 2025 23:52:52 +0000 Subject: [PATCH 31/38] refactor: Increase maxTokensPerRequest to 3000 and update related max_tokens settings Signed-off-by: Eden Reich --- examples/mcp/agents/nextjs/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/mcp/agents/nextjs/index.ts b/examples/mcp/agents/nextjs/index.ts index df3fc20..fe8c39d 100644 --- a/examples/mcp/agents/nextjs/index.ts +++ b/examples/mcp/agents/nextjs/index.ts @@ -57,7 +57,7 @@ class NextJSAgent { retryDelayMs: 60000, iterationCount: 0, totalTokensUsed: 0, - maxTokensPerRequest: 1000, + maxTokensPerRequest: 3000, maxHistoryLength: 10, sessionId: process.env.SESSION_ID || randomUUID(), memoryEnabled: true, @@ -700,7 +700,7 @@ Call save-state tool immediately with sessionId="${this.config.sessionId}" and t content: `Call save-state tool now with sessionId="${this.config.sessionId}"`, }, ], - max_tokens: 50, + max_tokens: this.config.maxTokensPerRequest, }, { onMCPTool: (toolCall) => { @@ -795,7 +795,7 @@ Call the save-state tool now.`, content: `Call save-state tool immediately with sessionId="${this.config.sessionId}". Do not respond with text - only call the tool.`, }, ], - max_tokens: 100, + max_tokens: this.config.maxTokensPerRequest, }, { onMCPTool: (toolCall) => { @@ -912,7 +912,7 @@ Call the save-state tool now.`, content: `Please restore the session state using the restore-state tool and provide the restored data.`, }, ], - max_tokens: 200, + max_tokens: this.config.maxTokensPerRequest, }, { onContent: (content) => { From 0800f85e31f81072fd7b9a72cb598daead15c483 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Mon, 2 Jun 2025 00:38:23 +0000 Subject: [PATCH 32/38] chore: Add tools for saving and managing conversation state in MCP Memory Server Signed-off-by: Eden Reich --- examples/mcp/agents/nextjs/index.ts | 22 +- examples/mcp/mcp-servers/memory/index.js | 543 +++++++++++++++++++++++ 2 files changed, 558 insertions(+), 7 deletions(-) diff --git a/examples/mcp/agents/nextjs/index.ts b/examples/mcp/agents/nextjs/index.ts index fe8c39d..ccba09a 100644 --- a/examples/mcp/agents/nextjs/index.ts +++ b/examples/mcp/agents/nextjs/index.ts @@ -273,7 +273,7 @@ If a Next.js project exists: * Always work in /tmp * If a project exists, enhance it — don't recreate -* Use Context7 tools for everything: tech decisions, patterns, and examples +* Always Use Context7 tools for everything: tech decisions, patterns, and examples * Adhere to modern best practices in project setup, UI/UX, and code quality `; } @@ -564,7 +564,7 @@ If a Next.js project exists: ); }, onReasoning: (reasoning) => { - console.log(`\n🤔 Agent Reasoning: ${reasoning}`); + process.stdout.write(`\n🤔 Agent Reasoning: ${reasoning}`); }, onContent: (content) => { process.stdout.write(content); @@ -715,8 +715,11 @@ Call save-state tool immediately with sessionId="${this.config.sessionId}" and t console.log('✅ State save tool invoked successfully'); } }, - onContent: () => { - // Suppress content output for memory saves + onReasoning: (reasoning) => { + process.stdout.write(`\n🤔 Memory Reasoning: ${reasoning}`); + }, + onContent: (content) => { + process.stdout.write(content); }, onError: (error) => { console.warn('⚠️ Memory save failed:', error.error); @@ -821,8 +824,8 @@ Call the save-state tool now.`, } } }, - onContent: () => { - // Suppress content output for memory saves + onContent: (content) => { + process.stdout.write(content); }, onError: (error) => { console.warn( @@ -915,6 +918,9 @@ Call the save-state tool now.`, max_tokens: this.config.maxTokensPerRequest, }, { + onReasoning: (reasoning) => { + process.stdout.write(`\n🤔 Memory Reasoning: ${reasoning}`); + }, onContent: (content) => { if (content.includes('{') && content.includes('}')) { try { @@ -923,7 +929,9 @@ Call the save-state tool now.`, restoredData = JSON.parse(jsonMatch[0]); } } catch { - // Ignore parsing errors + process.stderr.write( + `\n⚠️ Failed to parse restored data: ${content}\n` + ); } } }, diff --git a/examples/mcp/mcp-servers/memory/index.js b/examples/mcp/mcp-servers/memory/index.js index ce65783..23a1a07 100644 --- a/examples/mcp/mcp-servers/memory/index.js +++ b/examples/mcp/mcp-servers/memory/index.js @@ -472,6 +472,549 @@ function createMcpServer() { } }); + mcpServer.tool( + 'save-conversation', + { + sessionId: z.string().describe('Unique session identifier'), + messages: z + .array( + z.object({ + role: z + .enum(['user', 'assistant', 'system']) + .describe('Message role'), + content: z.string().describe('Message content'), + timestamp: z + .string() + .optional() + .describe('Message timestamp (ISO string)'), + }) + ) + .describe('Array of conversation messages'), + context: z.string().optional().describe('Optional context description'), + }, + async ({ sessionId, messages, context }) => { + const startTime = Date.now(); + + logger.info('save-conversation tool called', { + sessionId, + messageCount: messages.length, + hasContext: !!context, + roles: messages.map((m) => m.role), + }); + + try { + await ensureMemoryDir(); + + const timestampedMessages = messages.map((msg) => ({ + ...msg, + timestamp: msg.timestamp || new Date().toISOString(), + })); + + let existingData = {}; + const memoryPath = getMemoryPath(sessionId); + try { + const fileContent = await fs.readFile(memoryPath, 'utf8'); + existingData = JSON.parse(fileContent); + } catch { + logger.debug('No existing memory file found, creating new one', { + sessionId, + }); + } + + const memoryData = { + ...existingData, + sessionId, + conversation: { + messages: timestampedMessages, + context, + lastUpdated: new Date().toISOString(), + }, + timestamp: new Date().toISOString(), + }; + + logger.debug('Preparing conversation data for save', { + sessionId, + dataSize: JSON.stringify(memoryData).length, + messageCount: timestampedMessages.length, + }); + + await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); + + const duration = Date.now() - startTime; + + logger.info('Conversation saved successfully', { + sessionId, + memoryPath, + duration, + messageCount: timestampedMessages.length, + }); + + logMcpToolCall(logger, 'save-conversation', sessionId, { sessionId }); + + return { + content: [ + { + type: 'text', + text: `Conversation saved successfully for session: ${sessionId}. Saved ${timestampedMessages.length} messages.`, + }, + ], + }; + } catch (error) { + const duration = Date.now() - startTime; + + logger.error('Failed to save conversation', { + sessionId, + error: error.message, + stack: error.stack, + duration, + }); + + logMcpError(logger, error, { sessionId }); + logMcpToolCall(logger, 'save-conversation', sessionId, { sessionId }); + + return { + content: [ + { + type: 'text', + text: `Failed to save conversation: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + mcpServer.tool( + 'add-message', + { + sessionId: z.string().describe('Unique session identifier'), + role: z.enum(['user', 'assistant', 'system']).describe('Message role'), + content: z.string().describe('Message content'), + timestamp: z + .string() + .optional() + .describe('Message timestamp (ISO string)'), + }, + async ({ sessionId, role, content, timestamp }) => { + const startTime = Date.now(); + + logger.info('add-message tool called', { + sessionId, + role, + contentLength: content.length, + hasTimestamp: !!timestamp, + }); + + try { + await ensureMemoryDir(); + + const messageTimestamp = timestamp || new Date().toISOString(); + const newMessage = { + role, + content, + timestamp: messageTimestamp, + }; + + // Load existing memory data + let memoryData = {}; + const memoryPath = getMemoryPath(sessionId); + try { + const fileContent = await fs.readFile(memoryPath, 'utf8'); + memoryData = JSON.parse(fileContent); + } catch { + // File doesn't exist yet, create base structure + logger.debug('No existing memory file found, creating new one', { + sessionId, + }); + memoryData = { + sessionId, + timestamp: new Date().toISOString(), + }; + } + + // Initialize conversation structure if it doesn't exist + if (!memoryData.conversation) { + memoryData.conversation = { + messages: [], + context: null, + lastUpdated: new Date().toISOString(), + }; + } + + // Add the new message + memoryData.conversation.messages.push(newMessage); + memoryData.conversation.lastUpdated = new Date().toISOString(); + memoryData.timestamp = new Date().toISOString(); + + logger.debug('Preparing message data for save', { + sessionId, + role, + totalMessages: memoryData.conversation.messages.length, + }); + + await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); + + const duration = Date.now() - startTime; + + logger.info('Message added successfully', { + sessionId, + role, + duration, + totalMessages: memoryData.conversation.messages.length, + }); + + logMcpToolCall(logger, 'add-message', sessionId, { sessionId, role }); + + return { + content: [ + { + type: 'text', + text: `Message added successfully for session: ${sessionId}. Role: ${role}. Total messages: ${memoryData.conversation.messages.length}`, + }, + ], + }; + } catch (error) { + const duration = Date.now() - startTime; + + logger.error('Failed to add message', { + sessionId, + role, + error: error.message, + stack: error.stack, + duration, + }); + + logMcpError(logger, error, { sessionId, role }); + logMcpToolCall(logger, 'add-message', sessionId, { sessionId, role }); + + return { + content: [ + { + type: 'text', + text: `Failed to add message: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + mcpServer.tool( + 'get-conversation', + { + sessionId: z.string().describe('Unique session identifier'), + filterRole: z + .enum(['user', 'assistant', 'system']) + .optional() + .describe('Optional: filter messages by role'), + limit: z + .number() + .optional() + .describe( + 'Optional: limit number of messages returned (most recent first)' + ), + }, + async ({ sessionId, filterRole, limit }) => { + const startTime = Date.now(); + + logger.info('get-conversation tool called', { + sessionId, + filterRole, + limit, + }); + + try { + const memoryPath = getMemoryPath(sessionId); + + logger.debug('Attempting to read memory file for conversation', { + sessionId, + memoryPath, + }); + + try { + const fileContent = await fs.readFile(memoryPath, 'utf8'); + const memoryData = JSON.parse(fileContent); + + if (!memoryData.conversation || !memoryData.conversation.messages) { + const duration = Date.now() - startTime; + + logger.info('No conversation found for session', { + sessionId, + duration, + }); + + logMcpToolCall(logger, 'get-conversation', sessionId, { + sessionId, + }); + + return { + content: [ + { + type: 'text', + text: `No conversation found for session: ${sessionId}`, + }, + ], + }; + } + + let messages = [...memoryData.conversation.messages]; + + // Apply role filter if specified + if (filterRole) { + messages = messages.filter((msg) => msg.role === filterRole); + } + + // Apply limit if specified (get most recent messages) + if (limit && limit > 0) { + messages = messages.slice(-limit); + } + + const duration = Date.now() - startTime; + + logger.info('Conversation retrieved successfully', { + sessionId, + totalMessages: memoryData.conversation.messages.length, + filteredMessages: messages.length, + filterRole, + limit, + duration, + }); + + logMcpToolCall(logger, 'get-conversation', sessionId, { sessionId }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + sessionId: memoryData.sessionId, + conversation: { + messages, + context: memoryData.conversation.context, + lastUpdated: memoryData.conversation.lastUpdated, + totalMessages: memoryData.conversation.messages.length, + filteredMessages: messages.length, + }, + timestamp: memoryData.timestamp, + }, + null, + 2 + ), + }, + ], + }; + } catch (readError) { + if (readError.code === 'ENOENT') { + const duration = Date.now() - startTime; + + logger.info('No memory file found for session', { + sessionId, + duration, + }); + logMcpToolCall(logger, 'get-conversation', sessionId, { + sessionId, + }); + + return { + content: [ + { + type: 'text', + text: `No conversation found for session: ${sessionId}`, + }, + ], + }; + } + + logger.error('Failed to read memory file for conversation', { + sessionId, + memoryPath, + error: readError.message, + code: readError.code, + }); + + throw readError; + } + } catch (error) { + const duration = Date.now() - startTime; + + logger.error('Failed to get conversation', { + sessionId, + error: error.message, + stack: error.stack, + duration, + }); + + logMcpError(logger, error, { sessionId }); + logMcpToolCall(logger, 'get-conversation', sessionId, { sessionId }); + + return { + content: [ + { + type: 'text', + text: `Failed to get conversation: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + + mcpServer.tool( + 'clear-conversation', + { + sessionId: z.string().describe('Unique session identifier'), + keepOtherData: z + .boolean() + .optional() + .default(true) + .describe( + 'Whether to keep other session data (state, errors) and only clear conversation' + ), + }, + async ({ sessionId, keepOtherData = true }) => { + const startTime = Date.now(); + + logger.info('clear-conversation tool called', { + sessionId, + keepOtherData, + }); + + try { + const memoryPath = getMemoryPath(sessionId); + + if (keepOtherData) { + // Load existing data and only clear conversation + try { + const fileContent = await fs.readFile(memoryPath, 'utf8'); + const memoryData = JSON.parse(fileContent); + + // Clear conversation but keep other data + delete memoryData.conversation; + memoryData.timestamp = new Date().toISOString(); + + await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); + + const duration = Date.now() - startTime; + + logger.info( + 'Conversation cleared successfully (keeping other data)', + { + sessionId, + duration, + } + ); + + logMcpToolCall(logger, 'clear-conversation', sessionId, { + sessionId, + }); + + return { + content: [ + { + type: 'text', + text: `Conversation cleared successfully for session: ${sessionId} (other data preserved)`, + }, + ], + }; + } catch (readError) { + if (readError.code === 'ENOENT') { + const duration = Date.now() - startTime; + + logger.info('No conversation found to clear', { + sessionId, + duration, + }); + logMcpToolCall(logger, 'clear-conversation', sessionId, { + sessionId, + }); + + return { + content: [ + { + type: 'text', + text: `No conversation found to clear for session: ${sessionId}`, + }, + ], + }; + } + throw readError; + } + } else { + // Clear entire session file + try { + await fs.unlink(memoryPath); + + const duration = Date.now() - startTime; + + logger.info('Entire session cleared successfully', { + sessionId, + duration, + }); + logMcpToolCall(logger, 'clear-conversation', sessionId, { + sessionId, + }); + + return { + content: [ + { + type: 'text', + text: `Entire session cleared successfully: ${sessionId}`, + }, + ], + }; + } catch (unlinkError) { + if (unlinkError.code === 'ENOENT') { + const duration = Date.now() - startTime; + + logger.info('No session found to clear', { sessionId, duration }); + logMcpToolCall(logger, 'clear-conversation', sessionId, { + sessionId, + }); + + return { + content: [ + { + type: 'text', + text: `No session found to clear: ${sessionId}`, + }, + ], + }; + } + throw unlinkError; + } + } + } catch (error) { + const duration = Date.now() - startTime; + + logger.error('Failed to clear conversation', { + sessionId, + keepOtherData, + error: error.message, + code: error.code, + stack: error.stack, + duration, + }); + + logMcpError(logger, error, { sessionId }); + logMcpToolCall(logger, 'clear-conversation', sessionId, { sessionId }); + + return { + content: [ + { + type: 'text', + text: `Failed to clear conversation: ${error.message}`, + }, + ], + isError: true, + }; + } + } + ); + mcpServer.tool( 'clear-session', { From 4fab5aba260c9314715deaf3e06145ab019efefb Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Mon, 2 Jun 2025 00:40:56 +0000 Subject: [PATCH 33/38] refactor: Replace process.stdout.write with console.log for improved logging consistency Signed-off-by: Eden Reich --- examples/mcp/agents/nextjs/index.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/mcp/agents/nextjs/index.ts b/examples/mcp/agents/nextjs/index.ts index ccba09a..8fdaa91 100644 --- a/examples/mcp/agents/nextjs/index.ts +++ b/examples/mcp/agents/nextjs/index.ts @@ -564,7 +564,7 @@ If a Next.js project exists: ); }, onReasoning: (reasoning) => { - process.stdout.write(`\n🤔 Agent Reasoning: ${reasoning}`); + console.log(`\n🤔 Agent Reasoning: ${reasoning}`); }, onContent: (content) => { process.stdout.write(content); @@ -716,7 +716,7 @@ Call save-state tool immediately with sessionId="${this.config.sessionId}" and t } }, onReasoning: (reasoning) => { - process.stdout.write(`\n🤔 Memory Reasoning: ${reasoning}`); + console.log(`\n🤔 Memory Reasoning: ${reasoning}`); }, onContent: (content) => { process.stdout.write(content); @@ -919,7 +919,7 @@ Call the save-state tool now.`, }, { onReasoning: (reasoning) => { - process.stdout.write(`\n🤔 Memory Reasoning: ${reasoning}`); + console.log(`\n🤔 Memory Reasoning: ${reasoning}`); }, onContent: (content) => { if (content.includes('{') && content.includes('}')) { @@ -929,9 +929,7 @@ Call the save-state tool now.`, restoredData = JSON.parse(jsonMatch[0]); } } catch { - process.stderr.write( - `\n⚠️ Failed to parse restored data: ${content}\n` - ); + console.error(`⚠️ Failed to parse restored data: ${content}`); } } }, From 93ba90d2013c2c12f2210d0c8af64243cdcbef10 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Mon, 2 Jun 2025 01:58:15 +0000 Subject: [PATCH 34/38] refactor: Enhance KubernetesAgent with memory management features and optimize request handling Signed-off-by: Eden Reich --- examples/mcp/agents/kubernetes/index.ts | 592 +++++++++++++++++++- examples/mcp/agents/kubernetes/package.json | 8 +- examples/mcp/agents/nextjs/index.ts | 2 +- 3 files changed, 582 insertions(+), 20 deletions(-) diff --git a/examples/mcp/agents/kubernetes/index.ts b/examples/mcp/agents/kubernetes/index.ts index 9ab30bb..39ba61e 100644 --- a/examples/mcp/agents/kubernetes/index.ts +++ b/examples/mcp/agents/kubernetes/index.ts @@ -10,9 +10,11 @@ import { MessageRole, Provider, } from '@inference-gateway/sdk'; +import { randomUUID } from 'crypto'; import * as dotenv from 'dotenv'; import * as path from 'path'; import * as readline from 'readline'; +import { clearTimeout, setTimeout } from 'timers'; dotenv.config({ path: path.join(__dirname, '.env') }); @@ -28,6 +30,11 @@ interface AgentConfig { retryDelayMs: number; iterationCount: number; totalTokensUsed: number; + maxTokensPerRequest: number; + maxHistoryLength: number; + sessionId: string; + memoryEnabled: boolean; + abortController: globalThis.AbortController; } class KubernetesAgent { @@ -35,17 +42,26 @@ class KubernetesAgent { private rl: readline.Interface; constructor() { + console.log('🔧 Debug - Environment variables:'); + console.log(' PROVIDER:', process.env.PROVIDER); + console.log(' LLM:', process.env.LLM); + this.config = { client: new InferenceGatewayClient({ - baseURL: 'http://localhost:8080/v1', + baseURL: 'http://inference-gateway:8080/v1', }), provider: (process.env.PROVIDER as Provider) || Provider.groq, model: process.env.LLM || 'llama-3.3-70b-versatile', conversationHistory: [], maxRetries: 3, - retryDelayMs: 60000, + retryDelayMs: 10000, iterationCount: 0, totalTokensUsed: 0, + maxTokensPerRequest: 3000, + maxHistoryLength: 10, + sessionId: process.env.SESSION_ID || randomUUID(), + memoryEnabled: true, + abortController: new globalThis.AbortController(), }; this.rl = readline.createInterface({ @@ -60,8 +76,24 @@ class KubernetesAgent { } private async delay(ms: number): Promise { - return new Promise((resolve) => { - global.setTimeout(() => resolve(), ms); + return new Promise((resolve, reject) => { + if (this.config.abortController.signal.aborted) { + reject(new Error('Aborted')); + return; + } + + const timeout = global.setTimeout(() => resolve(), ms); + + const abortHandler = () => { + global.clearTimeout(timeout); + reject(new Error('Aborted')); + }; + + this.config.abortController.signal.addEventListener( + 'abort', + abortHandler, + { once: true } + ); }); } @@ -77,6 +109,14 @@ class KubernetesAgent { return ` You are an expert Kubernetes operations assistant with access to Context7 MCP tools for K8s documentation and research. Today is **June 1, 2025**. +**ABSOLUTELY CRITICAL - READ THIS FIRST**: +- You must NEVER output XML tags or function calls in any format +- You must NEVER use syntax like , , or +- Tools are handled automatically by the MCP system - you just describe what you need +- When you want to search: Say "I need to search for X" - don't output XML +- When you want to fetch: Say "I need to get information from Y" - don't output XML +- Just communicate naturally and the system will handle all tool calling + --- ### 🔧 CORE RESPONSIBILITIES @@ -118,6 +158,14 @@ You have access to several MCP tool categories: * Available for file operations in /tmp directory +**CRITICAL TOOL USAGE RULES**: +- NEVER use XML-style syntax like or tags +- NEVER output function calls in XML format like +- Tools are automatically available and will be called by the system when you need them +- Simply describe what you want to do and the system will handle tool calls +- If you need to search, just say "I need to search for..." and the system will call the appropriate tool +- If you need to fetch info, just say "I need to get..." and the system will call the appropriate tool + --- ### 🛡️ ERROR RECOVERY STRATEGY @@ -150,6 +198,8 @@ When encountering HTTP errors or failures: **When applying K8s configurations, always wait 10 seconds after operation.** +**CRITICAL: Never use XML-style tool syntax like \`\` or \`\` tags. All tools are automatically available through MCP and will be called by the LLM when needed. Simply describe what you want to do in natural language.** + 1. Clarify requirements and deployment architecture 2. Lookup Kubernetes and related technologies using Context7 tools 3. Retrieve current documentation, patterns, and best practices @@ -277,6 +327,22 @@ If Kubernetes configurations exist: ].includes(tool.name) ); + const memoryTools = tools.data.filter((tool) => + ['save-state', 'restore-state', 'list-sessions'].includes(tool.name) + ); + + if (memoryTools.length > 0) { + console.info( + `🧠 Found ${memoryTools.length} memory management tools` + ); + await this.loadStateFromMemory(); + } else { + console.info( + '⚠️ No memory tools available. State persistence disabled.' + ); + this.config.memoryEnabled = false; + } + const context7Tools = [...realContext7Tools, ...mockContext7Tools]; if (context7Tools.length === 0) { @@ -405,8 +471,24 @@ If Kubernetes configurations exist: case 'exit': case 'quit': console.log('\n👋 Thank you for using Kubernetes Agent! Goodbye!'); - this.rl.close(); - process.exit(0); + + if (this.config.memoryEnabled) { + console.log('💾 Saving session state before exit...'); + this.saveStateToMemoryForced('Manual exit via user command') + .then(() => { + console.log('✅ Session state saved successfully'); + this.rl.close(); + process.exit(0); + }) + .catch((error: Error) => { + console.warn('⚠️ Failed to save session state:', error.message); + this.rl.close(); + process.exit(0); + }); + } else { + this.rl.close(); + process.exit(0); + } return true; case 'clear': @@ -462,33 +544,91 @@ If Kubernetes configurations exist: console.log(`\n🔍 Processing Kubernetes request: "${userInput}"`); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + this.config.iterationCount++; + const iterationStartTime = Date.now(); + + console.log(`🔄 Starting Iteration #${this.config.iterationCount}`); + console.log( + `📝 User Input: "${userInput.substring(0, 100)}${userInput.length > 100 ? '...' : ''}"` + ); + console.log(`⏰ Start Time: ${new Date().toLocaleTimeString()}`); + console.log('─'.repeat(60)); + this.config.conversationHistory.push({ role: MessageRole.user, content: userInput, }); + if (this.config.memoryEnabled) { + await this.saveStateToMemory( + `Before processing request: "${userInput.substring(0, 50)}..."` + ); + } + + this.resetAbortController(); + let assistantResponse = ''; let shouldWaitForOperation = false; + console.log( + `🔧 Debug - Using provider: ${this.config.provider}, model: ${this.config.model}` + ); + await this.config.client.streamChatCompletion( { model: this.config.model, - messages: this.config.conversationHistory, - max_tokens: 2000, + messages: this.getOptimizedConversationHistory(), + max_tokens: this.config.maxTokensPerRequest, }, { onOpen: () => { console.log( - '🔗 Starting Kubernetes operations session with Context7...\n' + '\n🔗 Starting Kubernetes operations session with Context7...\n' ); }, onReasoning: (reasoning) => { - console.log(`\n🤔 Agent Reasoning: ${reasoning}`); + process.stdout.write(reasoning); }, onContent: (content) => { process.stdout.write(content); assistantResponse += content; }, + onUsageMetrics: (usage) => { + const iterationDuration = Date.now() - iterationStartTime; + this.config.totalTokensUsed += usage.total_tokens; + + console.log( + `\n\n💰 Iteration #${this.config.iterationCount} Token Usage:` + ); + console.log( + ` 📊 Prompt tokens: ${usage.prompt_tokens.toLocaleString()}` + ); + console.log( + ` ✍️ Completion tokens: ${usage.completion_tokens.toLocaleString()}` + ); + console.log( + ` 🎯 Total tokens: ${usage.total_tokens.toLocaleString()}` + ); + console.log(` ⏱️ Duration: ${iterationDuration}ms`); + console.log( + ` 🚀 Tokens/sec: ${Math.round((usage.total_tokens / iterationDuration) * 1000)}` + ); + + console.log(`\n📈 Cumulative Session Usage:`); + console.log(` 🔢 Total Iterations: ${this.config.iterationCount}`); + console.log( + ` 🎯 Total Tokens Used: ${this.config.totalTokensUsed.toLocaleString()}` + ); + console.log( + ` 📈 Average Tokens per Iteration: ${Math.round(this.config.totalTokensUsed / this.config.iterationCount).toLocaleString()}` + ); + + const estimatedCost = this.config.totalTokensUsed * 0.000001; + console.log( + ` 💰 Estimated Total Cost: $${estimatedCost.toFixed(6)}` + ); + console.log('─'.repeat(60)); + }, onMCPTool: (toolCall: any) => { console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); try { @@ -504,7 +644,11 @@ If Kubernetes configurations exist: toolCall.function.name.toLowerCase().includes('k8s') || toolCall.function.name.toLowerCase().includes('kubectl') || toolCall.function.name.toLowerCase().includes('deploy') || - toolCall.function.name.toLowerCase().includes('create') + toolCall.function.name.toLowerCase().includes('create') || + toolCall.function.name.toLowerCase().includes('apply') || + toolCall.function.name.toLowerCase().includes('helm') || + toolCall.function.name === 'write_file' || + toolCall.function.name === 'create_directory' ) { console.log( '☸️ Kubernetes operation detected - will wait 10 seconds after completion' @@ -514,6 +658,13 @@ If Kubernetes configurations exist: }, onError: (error) => { console.error(`\n❌ Stream Error: ${error.error}`); + + if (this.config.memoryEnabled) { + this.saveStateToMemory( + `Error occurred during request processing: ${error.error}` + ).catch(console.warn); + } + throw new Error(`Stream error: ${error.error}`); }, onFinish: async () => { @@ -532,13 +683,428 @@ If Kubernetes configurations exist: }); } }, - } + }, + this.config.provider, + this.config.abortController.signal ); } async shutdown(): Promise { + if (this.config.memoryEnabled) { + console.log('💾 Saving session state before shutdown...'); + try { + const shutdownTimeout = setTimeout(() => { + console.warn('⚠️ Shutdown timeout reached, forcing exit...'); + process.exit(1); + }, 10000); + + await this.saveStateToMemoryForced( + 'Manual shutdown via SIGINT/SIGTERM signal' + ); + + clearTimeout(shutdownTimeout); + console.log('✅ Session state saved successfully'); + } catch (error) { + console.warn( + '⚠️ Failed to save session state:', + (error as Error).message + ); + } + } + this.rl.close(); } + + abortOperations(): void { + if (!this.config.abortController.signal.aborted) { + this.config.abortController.abort('Shutdown signal received'); + } + } + + /** + * Save current state to memory MCP server + */ + private async saveStateToMemory(context: string): Promise { + if (!this.config.memoryEnabled) return; + + try { + const state = { + conversationHistory: this.config.conversationHistory.slice(-5), + iterationCount: this.config.iterationCount, + totalTokensUsed: this.config.totalTokensUsed, + timestamp: new Date().toISOString(), + }; + + console.log( + `💾 Saving state to memory for session: ${this.config.sessionId}` + ); + + let toolCallDetected = false; + let saveSuccessful = false; + + await this.config.client.streamChatCompletion( + { + model: this.config.model, + messages: [ + { + role: MessageRole.system, + content: `You are a memory manager. You MUST call the save-state tool now with the provided data. Don't explain - just call the tool immediately. + +SessionID: ${this.config.sessionId} +State: ${JSON.stringify(state)} +Context: ${context} + +Call save-state tool immediately with sessionId="${this.config.sessionId}" and the state object above.`, + }, + { + role: MessageRole.user, + content: `Call save-state tool now with sessionId="${this.config.sessionId}"`, + }, + ], + max_tokens: this.config.maxTokensPerRequest, + }, + { + onMCPTool: (toolCall) => { + toolCallDetected = true; + console.log(`📱 Memory tool called: ${toolCall.function.name}`); + + if ( + toolCall.function.name === 'save-state' || + toolCall.function.name === 'save-error-state' + ) { + saveSuccessful = true; + console.log('✅ State save tool invoked successfully'); + } + }, + onReasoning: (reasoning) => { + process.stdout.write(reasoning); + }, + onContent: (content) => { + process.stdout.write(content); + }, + onError: (error) => { + console.warn('⚠️ Memory save failed:', error.error); + }, + onFinish: () => { + if (toolCallDetected && saveSuccessful) { + console.log('✅ Memory save completed successfully'); + } else if (!toolCallDetected) { + console.warn( + '⚠️ No memory tool was called - memory may not be available' + ); + } else { + console.warn('⚠️ Memory tool called but save may have failed'); + } + }, + }, + this.config.provider, + this.config.abortController.signal + ); + } catch (error) { + console.warn( + '⚠️ Failed to save state to memory:', + (error as Error).message + ); + } + } + + /** + * Save current state to memory MCP server with forced tool usage + */ + private async saveStateToMemoryForced(context: string): Promise { + if (!this.config.memoryEnabled) return; + + try { + const state = { + conversationHistory: this.config.conversationHistory.slice(-5), + iterationCount: this.config.iterationCount, + totalTokensUsed: this.config.totalTokensUsed, + timestamp: new Date().toISOString(), + }; + + console.log( + `💾 Forcing memory save for session: ${this.config.sessionId}` + ); + + let toolCallDetected = false; + let saveSuccessful = false; + const maxAttempts = 3; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + if (this.config.abortController.signal.aborted) { + console.warn('⚠️ Memory save aborted during shutdown'); + throw new Error('Memory save aborted'); + } + + console.log(`🔄 Memory save attempt ${attempt}/${maxAttempts}`); + + try { + await this.config.client.streamChatCompletion( + { + model: this.config.model, + messages: [ + { + role: MessageRole.system, + content: `You are a memory manager. You MUST call the save-state tool immediately. No explanations, no acknowledgments - just call the tool. + +CRITICAL: You MUST call save-state tool with these exact parameters: +- sessionId: "${this.config.sessionId}" +- state: ${JSON.stringify(state)} +- context: "${context}" + +Call the save-state tool now.`, + }, + { + role: MessageRole.user, + content: `Call save-state tool immediately with sessionId="${this.config.sessionId}". Do not respond with text - only call the tool.`, + }, + ], + max_tokens: this.config.maxTokensPerRequest, + }, + { + onMCPTool: (toolCall) => { + toolCallDetected = true; + console.log(`📱 Memory tool called: ${toolCall.function.name}`); + + if ( + toolCall.function.name === 'save-state' || + toolCall.function.name === 'save-error-state' + ) { + saveSuccessful = true; + console.log('✅ Memory tool invoked successfully'); + try { + const args = JSON.parse(toolCall.function.arguments); + console.log( + `📝 Tool arguments:`, + JSON.stringify(args, null, 2) + ); + } catch { + console.error( + `📝 Raw tool arguments: ${toolCall.function.arguments}` + ); + } + } + }, + onContent: (content) => { + process.stdout.write(content); + }, + onError: (error) => { + console.warn( + `⚠️ Memory save attempt ${attempt} failed:`, + error.error + ); + }, + onFinish: () => { + if (toolCallDetected && saveSuccessful) { + console.log( + `✅ Memory save completed successfully on attempt ${attempt}` + ); + } else if (!toolCallDetected) { + console.warn( + `⚠️ Attempt ${attempt}: No memory tool was called` + ); + } else { + console.warn( + `⚠️ Attempt ${attempt}: Memory tool called but save may have failed` + ); + } + }, + }, + this.config.provider, + this.config.abortController.signal + ); + + if (toolCallDetected && saveSuccessful) { + break; + } + + if (attempt < maxAttempts) { + if (this.config.abortController.signal.aborted) { + console.warn('⚠️ Memory save aborted during retry wait'); + throw new Error('Memory save aborted'); + } + console.log(`⏳ Waiting 2 seconds before retry...`); + await this.delay(2000); + } + } catch (attemptError) { + console.warn( + `⚠️ Memory save attempt ${attempt} error:`, + (attemptError as Error).message + ); + if (attempt === maxAttempts) { + throw attemptError; + } + } + } + + if (!toolCallDetected || !saveSuccessful) { + console.error( + `❌ Failed to save memory after ${maxAttempts} attempts - memory tools may not be available` + ); + } + } catch (error) { + console.warn( + '⚠️ Failed to save state to memory:', + (error as Error).message + ); + } + } + + /** + * Load state from memory MCP server (via chat completion) + */ + private async loadStateFromMemory(): Promise { + if (!this.config.memoryEnabled) return false; + + try { + console.log( + `📥 Attempting to restore state for session: ${this.config.sessionId}` + ); + + let restoredData: any = null; + + await this.config.client.streamChatCompletion( + { + model: this.config.model, + messages: [ + { + role: MessageRole.system, + content: `You have access to memory management tools. Restore the saved state for session "${this.config.sessionId}".`, + }, + { + role: MessageRole.user, + content: `Please restore the session state using the restore-state tool and provide the restored data.`, + }, + ], + max_tokens: this.config.maxTokensPerRequest, + }, + { + onReasoning: (reasoning) => { + process.stdout.write(reasoning); + }, + onContent: (content) => { + if (content.includes('{') && content.includes('}')) { + try { + const jsonMatch = content.match(/\{[\s\S]*\}/); + if (jsonMatch) { + restoredData = JSON.parse(jsonMatch[0]); + } + } catch { + console.error(`⚠️ Failed to parse restored data: ${content}`); + } + } + }, + onMCPTool: (toolCall) => { + console.log(`📱 Memory tool called: ${toolCall.function.name}`); + }, + onError: () => { + console.log('ℹ️ No previous state found'); + }, + onFinish: () => { + if (restoredData && restoredData.state) { + this.config.conversationHistory = + restoredData.state.conversationHistory || []; + this.config.iterationCount = + restoredData.state.iterationCount || 0; + this.config.totalTokensUsed = + restoredData.state.totalTokensUsed || 0; + + console.log( + `✅ Restored state from ${restoredData.state.timestamp}` + ); + console.log( + `📊 Restored ${this.config.conversationHistory.length} messages` + ); + console.log( + `🔢 Restored iteration count: ${this.config.iterationCount}` + ); + } + }, + }, + this.config.provider, + this.config.abortController.signal + ); + + return !!restoredData; + } catch (error) { + console.log(`ℹ️ No previous state found: ${(error as Error).message}`); + return false; + } + } + + /** + * Truncate conversation history to stay within token limits + */ + private truncateConversationHistory(): void { + if ( + this.config.conversationHistory.length <= + this.config.maxHistoryLength + 1 + ) { + return; + } + + console.log( + `✂️ Truncating conversation history from ${this.config.conversationHistory.length} to ${this.config.maxHistoryLength + 1} messages` + ); + + const systemPrompt = this.config.conversationHistory[0]; + + const recentMessages = this.config.conversationHistory.slice( + -this.config.maxHistoryLength + ); + + const truncatedMessages = this.config.conversationHistory.slice( + 1, + -this.config.maxHistoryLength + ); + + if (truncatedMessages.length > 0) { + this.saveStateToMemoryForced( + `Truncated ${truncatedMessages.length} older messages` + ).catch((error) => { + console.warn( + '⚠️ Failed to save truncated messages to memory:', + (error as Error).message + ); + }); + } + + this.config.conversationHistory = [systemPrompt, ...recentMessages]; + } + + /** + * Estimate token count for a message (rough approximation) + */ + private estimateTokenCount(text: string): number { + // Rough approximation: 1 token ≈ 4 characters + return Math.ceil(text.length / 4); + } + + /** + * Get optimized conversation history for the current request + */ + private getOptimizedConversationHistory(): Array<{ + role: MessageRole; + content: string; + }> { + this.truncateConversationHistory(); + + const totalEstimatedTokens = this.config.conversationHistory.reduce( + (sum, msg) => sum + this.estimateTokenCount(msg.content), + 0 + ); + + console.log(`📊 Estimated tokens in conversation: ${totalEstimatedTokens}`); + + return this.config.conversationHistory; + } + + private resetAbortController(): void { + if (!this.config.abortController.signal.aborted) { + this.config.abortController.abort('Starting new request'); + } + this.config.abortController = new globalThis.AbortController(); + } } async function runKubernetesAgent(): Promise { @@ -546,12 +1112,14 @@ async function runKubernetesAgent(): Promise { process.on('SIGINT', async () => { console.log('\n\n👋 Shutting down Kubernetes Agent...'); + agent.abortOperations(); await agent.shutdown(); process.exit(0); }); process.on('SIGTERM', async () => { console.log('\n\n👋 Shutting down Kubernetes Agent...'); + agent.abortOperations(); await agent.shutdown(); process.exit(0); }); diff --git a/examples/mcp/agents/kubernetes/package.json b/examples/mcp/agents/kubernetes/package.json index 38612e4..cd8dcdf 100644 --- a/examples/mcp/agents/kubernetes/package.json +++ b/examples/mcp/agents/kubernetes/package.json @@ -5,13 +5,7 @@ "main": "index.js", "private": true, "scripts": { - "clean": "rm -rf shared/next-app shared/update-check", - "example:nextjs-agent": "tsx example-nextjs-agent.ts", - "example:vite-agent": "tsx example-vite-agent.ts", - "example:kubernetes-agent": "tsx example-kubernetes-agent.ts", - "compose:up": "docker-compose up -d", - "compose:down": "docker-compose down", - "compose:logs": "docker-compose logs -f" + "start": "tsx index.ts" }, "keywords": [], "author": "", diff --git a/examples/mcp/agents/nextjs/index.ts b/examples/mcp/agents/nextjs/index.ts index 8fdaa91..f10d8cf 100644 --- a/examples/mcp/agents/nextjs/index.ts +++ b/examples/mcp/agents/nextjs/index.ts @@ -54,7 +54,7 @@ class NextJSAgent { model: process.env.LLM || 'llama-3.3-70b-versatile', conversationHistory: [], maxRetries: 3, - retryDelayMs: 60000, + retryDelayMs: 10000, iterationCount: 0, totalTokensUsed: 0, maxTokensPerRequest: 3000, From 0d6bf6b817f46b264095a3472da8b1acc0bd507b Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Mon, 2 Jun 2025 02:25:36 +0000 Subject: [PATCH 35/38] refactor: Refactor MCP Memory Server to use MemoryManager for in-memory operations - Removed healthcheck from docker-compose.yml for the MCP service. - Introduced MemoryManager class for efficient in-memory storage with background persistence. - Replaced file I/O operations with in-memory methods in index.js for saving, restoring, and managing session data. - Implemented automatic background persistence for dirty sessions in MemoryManager. - Enhanced error handling and logging throughout the memory management process. - Updated server startup and shutdown procedures to integrate MemoryManager lifecycle. Signed-off-by: Eden Reich --- examples/mcp/agents/kubernetes/index.ts | 493 ++++++++----- examples/mcp/agents/nextjs/index.ts | 187 +---- examples/mcp/docker-compose.yml | 6 - examples/mcp/mcp-servers/memory/index.js | 696 ++++++------------ .../mcp/mcp-servers/memory/memory-manager.js | 529 +++++++++++++ 5 files changed, 1059 insertions(+), 852 deletions(-) create mode 100644 examples/mcp/mcp-servers/memory/memory-manager.js diff --git a/examples/mcp/agents/kubernetes/index.ts b/examples/mcp/agents/kubernetes/index.ts index 39ba61e..04843e2 100644 --- a/examples/mcp/agents/kubernetes/index.ts +++ b/examples/mcp/agents/kubernetes/index.ts @@ -21,6 +21,19 @@ dotenv.config({ path: path.join(__dirname, '.env') }); declare const require: any; declare const module: any; +interface ErrorRecord { + timestamp: string; + iterationCount: number; + errorType: 'stream_error' | 'tool_error' | 'mcp_error' | 'memory_error'; + errorMessage: string; + context: string; + toolName?: string; + toolId?: string; + toolArguments?: any; + userInput?: string; + recoveryAttempted: boolean; +} + interface AgentConfig { client: InferenceGatewayClient; provider: Provider; @@ -35,6 +48,13 @@ interface AgentConfig { sessionId: string; memoryEnabled: boolean; abortController: globalThis.AbortController; + errorHistory: ErrorRecord[]; + lastFailedToolCall?: { + name: string; + id: string; + arguments: any; + timestamp: string; + }; } class KubernetesAgent { @@ -62,6 +82,8 @@ class KubernetesAgent { sessionId: process.env.SESSION_ID || randomUUID(), memoryEnabled: true, abortController: new globalThis.AbortController(), + errorHistory: [], + lastFailedToolCall: undefined, }; this.rl = readline.createInterface({ @@ -105,199 +127,247 @@ class KubernetesAgent { console.log('✅ Kubernetes operation wait period completed.\n'); } - private getSystemPrompt(): string { - return ` -You are an expert Kubernetes operations assistant with access to Context7 MCP tools for K8s documentation and research. Today is **June 1, 2025**. - -**ABSOLUTELY CRITICAL - READ THIS FIRST**: -- You must NEVER output XML tags or function calls in any format -- You must NEVER use syntax like , , or -- Tools are handled automatically by the MCP system - you just describe what you need -- When you want to search: Say "I need to search for X" - don't output XML -- When you want to fetch: Say "I need to get information from Y" - don't output XML -- Just communicate naturally and the system will handle all tool calling - ---- - -### 🔧 CORE RESPONSIBILITIES - -You help users with **Kubernetes cluster operations and container orchestration** by: - -1. Understanding deployment requirements and recommending optimal K8s strategies -2. Using **Context7 tools** to retrieve up-to-date Kubernetes documentation and best practices -3. Creating production-ready YAML manifests and Helm charts -4. Following Kubernetes security and performance conventions -5. Providing cluster management, monitoring, and troubleshooting guidance - ---- - -### 🧰 AVAILABLE TOOLS - -You have access to several MCP tool categories: - -**Context7 Tools (@upstash/context7-mcp):** - -* c41_resolve-library-id: Resolve technology names to Context7-compatible IDs -* c41_get-library-docs: Fetch full documentation, usage examples, and best practices - -**Mock Tools (for local/demo use):** - -* search_libraries: Search for libraries by name or functionality -* get_library_details: Fetch library metadata and features -* get_documentation: Fetch usage examples and implementation patterns - -**Memory Tools (for error recovery):** - -* save-state: Save current progress/state with a session ID -* save-error-state: Save state when HTTP errors occur for recovery -* restore-state: Restore previously saved state by session ID -* list-sessions: List all saved sessions -* clear-session: Remove a saved session - -**File System Tools:** - -* Available for file operations in /tmp directory - -**CRITICAL TOOL USAGE RULES**: -- NEVER use XML-style syntax like or tags -- NEVER output function calls in XML format like -- Tools are automatically available and will be called by the system when you need them -- Simply describe what you want to do and the system will handle tool calls -- If you need to search, just say "I need to search for..." and the system will call the appropriate tool -- If you need to fetch info, just say "I need to get..." and the system will call the appropriate tool - ---- - -### 🛡️ ERROR RECOVERY STRATEGY - -When encountering HTTP errors or failures: - -1. Immediately save state using save-error-state with: - - Unique session ID (e.g., "k8s-task-{timestamp}") - - Current progress/context - - Error details -2. In subsequent runs, check for existing sessions with list-sessions -3. Restore state if needed and continue from where you left off -4. Clear sessions when tasks complete successfully - ---- - -### 📂 FILE SYSTEM RULES - -* All Kubernetes manifests and generated files must **use the /tmp directory exclusively**. -* If **Kubernetes configurations already exist in /tmp**, continue working within them instead of creating new ones. -* You must **never overwrite** existing configurations unless explicitly asked. - ---- + /** + * Record an error to both local history and memory + */ + private async recordError( + errorType: ErrorRecord['errorType'], + errorMessage: string, + context: string, + toolCall?: any, + userInput?: string + ): Promise { + const errorRecord: ErrorRecord = { + timestamp: new Date().toISOString(), + iterationCount: this.config.iterationCount, + errorType, + errorMessage, + context, + toolName: toolCall?.function?.name, + toolId: toolCall?.id, + toolArguments: toolCall?.function?.arguments + ? JSON.parse(toolCall.function.arguments) + : undefined, + userInput: userInput?.substring(0, 100), + recoveryAttempted: false, + }; -### ⚙️ DEVELOPMENT WORKFLOW + this.config.errorHistory.push(errorRecord); -**Always use Context7 tools before creating K8s resources:** + // Keep only last 10 errors to prevent memory bloat + if (this.config.errorHistory.length > 10) { + this.config.errorHistory = this.config.errorHistory.slice(-10); + } -**Always list the files in a directory before creating new manifests.** + console.log(`📝 Recording ${errorType}: ${errorMessage}`); -**When applying K8s configurations, always wait 10 seconds after operation.** + // Save to memory if enabled + if (this.config.memoryEnabled) { + await this.saveErrorToMemory(errorRecord); + } + } -**CRITICAL: Never use XML-style tool syntax like \`\` or \`\` tags. All tools are automatically available through MCP and will be called by the LLM when needed. Simply describe what you want to do in natural language.** + /** + * Save error state to memory with detailed context + */ + private async saveErrorToMemory(errorRecord: ErrorRecord): Promise { + if (!this.config.memoryEnabled) return; -1. Clarify requirements and deployment architecture -2. Lookup Kubernetes and related technologies using Context7 tools -3. Retrieve current documentation, patterns, and best practices -4. Create or enhance configurations under /tmp, maintaining clean structure -5. Follow K8s conventions, security policies, and resource management -6. Include proper monitoring, logging, and health check configurations -7. Prioritize scalability, reliability, and operational excellence + try { + const errorState = { + conversationHistory: this.config.conversationHistory.slice(-3), + iterationCount: this.config.iterationCount, + totalTokensUsed: this.config.totalTokensUsed, + timestamp: new Date().toISOString(), + errorHistory: this.config.errorHistory, + lastError: errorRecord, + }; ---- + console.log( + `🚨 Saving error state to memory for session: ${this.config.sessionId}` + ); -### ☸️ KUBERNETES RESOURCE RULES + let toolCallDetected = false; + let saveSuccessful = false; -* **Use the latest Kubernetes API versions and best practices** -* **Follow security-first approach with RBAC, network policies, and pod security** -* **Structure should include:** - * Namespace definitions - * Deployment/StatefulSet manifests - * Service and Ingress configurations - * ConfigMaps and Secrets - * RBAC policies (ServiceAccount, Role, RoleBinding) - * NetworkPolicies for security - * HorizontalPodAutoscaler for scaling - * PodDisruptionBudget for availability + await this.config.client.streamChatCompletion( + { + model: this.config.model, + messages: [ + { + role: MessageRole.system, + content: `You are a memory manager handling error recovery. You MUST call the save-error-state tool immediately with the error details below. -**Supported Kubernetes Resources:** -* **Workloads:** Deployments, StatefulSets, DaemonSets, Jobs, CronJobs -* **Services:** ClusterIP, NodePort, LoadBalancer, ExternalName -* **Configuration:** ConfigMaps, Secrets, PersistentVolumes -* **Security:** RBAC, NetworkPolicies, PodSecurityPolicies -* **Scaling:** HPA, VPA, Cluster Autoscaler -* **Networking:** Ingress, Service Mesh (Istio/Linkerd) +CRITICAL ERROR OCCURRED: +Error Type: ${errorRecord.errorType} +Error Message: ${errorRecord.errorMessage} +Context: ${errorRecord.context} +Tool: ${errorRecord.toolName || 'N/A'} +Tool ID: ${errorRecord.toolId || 'N/A'} +User Input: ${errorRecord.userInput || 'N/A'} +Timestamp: ${errorRecord.timestamp} -If Kubernetes configurations exist: -* Validate API versions and resource definitions -* Extend or modify as needed based on requirements -* Optimize for performance, security, and cost +SessionID: ${this.config.sessionId} +Error State: ${JSON.stringify(errorState)} ---- +Call save-error-state tool immediately with sessionId="${this.config.sessionId}" and the error state above.`, + }, + { + role: MessageRole.user, + content: `URGENT: Save error state now using save-error-state tool for session "${this.config.sessionId}". Include full error context.`, + }, + ], + max_tokens: this.config.maxTokensPerRequest, + }, + { + onMCPTool: (toolCall) => { + toolCallDetected = true; + console.log( + `📱 Error memory tool called: ${toolCall.function.name}` + ); -### 🧪 KUBERNETES ECOSYSTEM (verify latest versions with Context7) + if ( + toolCall.function.name === 'save-error-state' || + toolCall.function.name === 'save-state' + ) { + saveSuccessful = true; + console.log('✅ Error state save tool invoked successfully'); + } + }, + onReasoning: () => { + // Suppress reasoning output for error saves to reduce noise + }, + onContent: () => { + // Suppress content output for error saves to reduce noise + }, + onError: (error) => { + console.warn('⚠️ Error memory save failed:', error.error); + }, + onFinish: () => { + if (toolCallDetected && saveSuccessful) { + console.log('✅ Error state saved to memory successfully'); + } else { + console.warn('⚠️ Failed to save error state to memory'); + } + }, + }, + this.config.provider, + this.config.abortController.signal + ); + } catch (error) { + console.warn( + '⚠️ Failed to save error state to memory:', + (error as Error).message + ); + } + } -**Core:** kubectl, kubelet, kube-apiserver, etcd, kube-controller-manager -**Container Runtime:** containerd, Docker, CRI-O -**Networking:** Calico, Flannel, Weave, Cilium -**Service Mesh:** Istio, Linkerd, Consul Connect -**Monitoring:** Prometheus, Grafana, Jaeger, Kiali -**CI/CD:** ArgoCD, Flux, Tekton, Jenkins X -**Package Management:** Helm, Kustomize, Operator Framework -**Security:** Falco, Open Policy Agent (OPA), Twistlock -**Storage:** Longhorn, Rook, OpenEBS, Portworx + /** + * Load and process previous errors from memory + */ + private async loadErrorHistory(): Promise { + if (!this.config.memoryEnabled) return; ---- + try { + console.log( + `📥 Loading error history for session: ${this.config.sessionId}` + ); -### 🚀 COMMON KUBERNETES PATTERNS TO LEVERAGE + let errorData: any = null; -* **Microservices Architecture** with proper service decomposition -* **GitOps Deployment** with declarative configurations -* **Blue-Green Deployments** for zero-downtime updates -* **Canary Releases** for safe rollouts -* **Resource Quotas** and limits for multi-tenancy -* **Health Checks** (liveness, readiness, startup probes) -* **Secrets Management** with external secret operators -* **Observability** with distributed tracing and metrics + await this.config.client.streamChatCompletion( + { + model: this.config.model, + messages: [ + { + role: MessageRole.system, + content: `You have access to memory management tools. Check for and restore any saved error states for session "${this.config.sessionId}". If errors were previously recorded, provide details about what went wrong.`, + }, + { + role: MessageRole.user, + content: `Check memory for previous errors in session "${this.config.sessionId}" and restore any error states found.`, + }, + ], + max_tokens: this.config.maxTokensPerRequest, + }, + { + onContent: (content) => { + if (content.includes('error') || content.includes('Error')) { + console.log( + `📋 Previous error context: ${content.substring(0, 200)}...` + ); + } + if (content.includes('{') && content.includes('}')) { + try { + const jsonMatch = content.match(/\{[\s\S]*\}/); + if (jsonMatch) { + errorData = JSON.parse(jsonMatch[0]); + } + } catch { + // Ignore parsing errors for error recovery + } + } + }, + onMCPTool: (toolCall) => { + console.log( + `📱 Error recovery tool called: ${toolCall.function.name}` + ); + }, + onError: () => { + console.log('ℹ️ No previous error state found'); + }, + onFinish: () => { + if (errorData && errorData.errorHistory) { + this.config.errorHistory = errorData.errorHistory || []; + console.log( + `✅ Restored ${this.config.errorHistory.length} previous error records` + ); ---- + if (this.config.errorHistory.length > 0) { + const lastError = + this.config.errorHistory[this.config.errorHistory.length - 1]; + console.log( + `🔍 Last error was: ${lastError.errorType} - ${lastError.errorMessage}` + ); + console.log(` Context: ${lastError.context}`); + if (lastError.toolName) { + console.log( + ` Failed tool: ${lastError.toolName} (${lastError.toolId})` + ); + } + } + } + }, + }, + this.config.provider, + this.config.abortController.signal + ); + } catch (error) { + console.log(`ℹ️ No error history found: ${(error as Error).message}`); + } + } -### 🛡️ SECURITY BEST PRACTICES + private getSystemPrompt(): string { + let errorHistoryPrompt = ''; -* **Principle of Least Privilege** with RBAC -* **Network Segmentation** with NetworkPolicies -* **Pod Security Standards** enforcement -* **Image Security** scanning and admission controllers -* **Secret Rotation** and external secret management -* **Audit Logging** for compliance and monitoring -* **Resource Isolation** with namespaces and quotas + if (this.config.errorHistory.length > 0) { + const recentErrors = this.config.errorHistory.slice(-2); + errorHistoryPrompt = `\nERROR CONTEXT: Previous ${recentErrors.length} errors encountered. Adapt approach accordingly.\n`; + } ---- + return `You are a Kubernetes operations assistant. Help with cluster management and container orchestration.${errorHistoryPrompt} -### 📊 OPERATIONAL EXCELLENCE +CORE RULES: +- Work only in /tmp directory +- If K8s configs exist, enhance them - don't recreate +- Use Context7 tools for documentation lookup +- Tools called automatically - just describe what you need +- Follow security-first approach with RBAC and network policies +- Wait 10 seconds after applying K8s configurations -* **Infrastructure as Code** with Terraform/Pulumi -* **Automated Scaling** based on metrics -* **Disaster Recovery** planning and testing -* **Cost Optimization** with resource right-sizing -* **Performance Monitoring** and alerting -* **Capacity Planning** for growth -* **Multi-cluster Management** for resilience +AVAILABLE TOOLS: Context7 docs, filesystem, memory tools for recovery ---- - -### ✅ SUMMARY - -* Always work in /tmp -* If K8s configurations exist, enhance them — don't recreate -* Use Context7 tools for everything: K8s decisions, patterns, and examples -* Follow security-first, cloud-native principles -* Adhere to modern best practices in cluster operations, security, and reliability -`; +WORKFLOW: 1) Clarify requirements 2) Use Context7 for K8s docs 3) Create manifests in /tmp 4) Follow best practices`; } async initialize(): Promise { @@ -335,7 +405,8 @@ If Kubernetes configurations exist: console.info( `🧠 Found ${memoryTools.length} memory management tools` ); - await this.loadStateFromMemory(); + // Skip slow memory restoration for faster startup - only load on demand + console.log('📥 Skipping memory restoration for faster startup'); } else { console.info( '⚠️ No memory tools available. State persistence disabled.' @@ -639,6 +710,14 @@ If Kubernetes configurations exist: } console.log(`🔍 Tool ID: ${toolCall.id}\n`); + // Store the tool call details for error recovery if needed + this.config.lastFailedToolCall = { + name: toolCall.function.name, + id: toolCall.id, + arguments: toolCall.function.arguments, + timestamp: new Date().toISOString(), + }; + if ( toolCall.function.name.toLowerCase().includes('kubernetes') || toolCall.function.name.toLowerCase().includes('k8s') || @@ -656,16 +735,21 @@ If Kubernetes configurations exist: shouldWaitForOperation = true; } }, - onError: (error) => { + onError: async (error) => { console.error(`\n❌ Stream Error: ${error.error}`); - if (this.config.memoryEnabled) { - this.saveStateToMemory( - `Error occurred during request processing: ${error.error}` - ).catch(console.warn); - } + // Record the error with detailed context + await this.recordError( + 'stream_error', + error.error || 'Unknown stream error', + `Stream error during iteration ${this.config.iterationCount}. Last tool call: ${this.config.lastFailedToolCall?.name || 'none'}`, + this.config.lastFailedToolCall, + userInput + ); - throw new Error(`Stream error: ${error.error}`); + throw new Error( + `Stream error: ${error.error || 'Unknown stream error'}` + ); }, onFinish: async () => { console.log('\n\n✅ Kubernetes operations session completed!\n'); @@ -951,17 +1035,19 @@ Call the save-state tool now.`, } /** - * Load state from memory MCP server (via chat completion) + * Combined fast memory state and error history loading */ - private async loadStateFromMemory(): Promise { - if (!this.config.memoryEnabled) return false; + private async quickLoadStateAndErrorHistory(): Promise { + if (!this.config.memoryEnabled) return; try { console.log( `📥 Attempting to restore state for session: ${this.config.sessionId}` ); + // Single LLM call to check both state and errors let restoredData: any = null; + let errorData: any = null; await this.config.client.streamChatCompletion( { @@ -969,28 +1055,31 @@ Call the save-state tool now.`, messages: [ { role: MessageRole.system, - content: `You have access to memory management tools. Restore the saved state for session "${this.config.sessionId}".`, + content: `You have access to memory management tools. Quickly restore the saved state for session "${this.config.sessionId}" and check for any previous errors. Use the restore-state tool once.`, }, { role: MessageRole.user, - content: `Please restore the session state using the restore-state tool and provide the restored data.`, + content: `Restore session state and error history for "${this.config.sessionId}" using restore-state tool.`, }, ], - max_tokens: this.config.maxTokensPerRequest, + max_tokens: 2000, // Reduced token limit for faster response }, { - onReasoning: (reasoning) => { - process.stdout.write(reasoning); - }, onContent: (content) => { if (content.includes('{') && content.includes('}')) { try { const jsonMatch = content.match(/\{[\s\S]*\}/); if (jsonMatch) { - restoredData = JSON.parse(jsonMatch[0]); + const data = JSON.parse(jsonMatch[0]); + if (data.state) { + restoredData = data; + } + if (data.errorHistory || data.lastError) { + errorData = data; + } } } catch { - console.error(`⚠️ Failed to parse restored data: ${content}`); + // Ignore parsing errors for quick startup } } }, @@ -1001,6 +1090,7 @@ Call the save-state tool now.`, console.log('ℹ️ No previous state found'); }, onFinish: () => { + // Restore state if found if (restoredData && restoredData.state) { this.config.conversationHistory = restoredData.state.conversationHistory || []; @@ -1009,9 +1099,7 @@ Call the save-state tool now.`, this.config.totalTokensUsed = restoredData.state.totalTokensUsed || 0; - console.log( - `✅ Restored state from ${restoredData.state.timestamp}` - ); + console.log(`✅ Restored state from ${restoredData.timestamp}`); console.log( `📊 Restored ${this.config.conversationHistory.length} messages` ); @@ -1019,16 +1107,33 @@ Call the save-state tool now.`, `🔢 Restored iteration count: ${this.config.iterationCount}` ); } + + // Restore error history if found + if (errorData && errorData.errorHistory) { + this.config.errorHistory = errorData.errorHistory || []; + console.log( + `✅ Restored ${this.config.errorHistory.length} previous error records` + ); + + if (this.config.errorHistory.length > 0) { + const lastError = + this.config.errorHistory[this.config.errorHistory.length - 1]; + console.log( + `🔍 Last error was: ${lastError.errorType} - ${lastError.errorMessage}` + ); + } + } + + if (!restoredData && !errorData) { + console.log('ℹ️ No previous state found'); + } }, }, this.config.provider, this.config.abortController.signal ); - - return !!restoredData; } catch (error) { console.log(`ℹ️ No previous state found: ${(error as Error).message}`); - return false; } } diff --git a/examples/mcp/agents/nextjs/index.ts b/examples/mcp/agents/nextjs/index.ts index f10d8cf..abcb538 100644 --- a/examples/mcp/agents/nextjs/index.ts +++ b/examples/mcp/agents/nextjs/index.ts @@ -1,7 +1,12 @@ /** * Interactive NextJS Agent * - * This agent allows users to interactively request app development assistance + * This agent allows maxRetries: 3, + retryDelayMs: 2000, + iterationCount: 0, + totalTokensUsed: 0, + maxTokensPerRequest: 4000, // Reduced from 16000 + maxHistoryLength: 8, // Reduced from 12 to interactively request app development assistance * using Context7 MCP tools for up-to-date documentation and library information. */ @@ -106,176 +111,19 @@ class NextJSAgent { } private getSystemPrompt(): string { - return ` -You are an expert software development assistant with access to Context7 MCP tools for library documentation and research. Today is **June 1, 2025**. - -**ABSOLUTELY CRITICAL - READ THIS FIRST**: -- You must NEVER output XML tags or function calls in any format -- You must NEVER use syntax like , , or -- Tools are handled automatically by the MCP system - you just describe what you need -- When you want to search: Say "I need to search for X" - don't output XML -- When you want to fetch: Say "I need to get information from Y" - don't output XML -- Just communicate naturally and the system will handle all tool calling - ---- - -### 🔧 CORE RESPONSIBILITIES - -You help users create **modern, production-grade applications** by: - -1. Understanding user requirements and recommending the best-fit technologies -2. Using **Context7 tools** to retrieve up-to-date documentation and best practices -3. Building complete projects with proper structure and configuration -4. Following modern development conventions and patterns -5. Creating clean, responsive, and accessible UI/UX - ---- - -### 🧰 AVAILABLE TOOLS - -You have access to several MCP tool categories: - -**Context7 Tools (@upstash/context7-mcp):** - -* c41_resolve-library-id: Resolve technology names to Context7-compatible IDs -* c41_get-library-docs: Fetch full documentation, usage examples, and best practices - -**Web Search Tools:** - -* search_web: Perform web searches using DuckDuckGo (use this FIRST before fetching URLs) -* fetch_url: Fetch content from verified URLs (only use URLs from search results) -* get_page_title: Extract page titles from URLs - -**Mock Tools (for local/demo use):** - -* search_libraries: Search for libraries by name or functionality -* get_library_details: Fetch library metadata and features -* get_documentation: Fetch usage examples and implementation patterns - -**Memory Tools (for error recovery):** - -* save-state: Save current progress/state with a session ID -* save-error-state: Save state when HTTP errors occur for recovery -* restore-state: Restore previously saved state by session ID -* list-sessions: List all saved sessions -* clear-session: Remove a saved session - -**File System Tools:** - -* read_file: Read the contents of a file -* write_file: Write content to a file -* list_directory: List directory contents -* create_directory: Create directories -* delete_file: Delete files -* file_info: Get file information - -**NPM Tools:** - -* npm_run: Execute npm commands (install, build, start, test, etc.) -* npm_init: Initialize new npm project -* npm_install: Install npm packages -* create_nextjs_project: Create a new Next.js project with specified options - -**CRITICAL TOOL USAGE RULES**: -- NEVER use XML-style syntax like or tags -- NEVER output function calls in XML format like -- Tools are automatically available and will be called by the system when you need them -- Simply describe what you want to do and the system will handle tool calls -- If you need to search, just say "I need to search for..." and the system will call search_web -- If you need to fetch a URL, just say "I need to fetch..." and the system will call fetch_url - ---- - -### 🛡️ ERROR RECOVERY STRATEGY - -When encountering HTTP errors or failures: - -1. Immediately save state using save-error-state with: - - Unique session ID (e.g., "nextjs-task-{timestamp}") - - Current progress/context - - Error details -2. In subsequent runs, check for existing sessions with list-sessions -3. Restore state if needed and continue from where you left off -4. Clear sessions when tasks complete successfully - ---- - -### 📂 FILE SYSTEM RULES - -* All projects and generated files must **use the /tmp directory exclusively**. -* If a **Next.js project already exists in /tmp**, continue working within it instead of creating a new one. -* You must **never overwrite** an existing project unless explicitly asked. - ---- - -### ⚙️ DEVELOPMENT WORKFLOW - -**Always use Context7 tools before coding:** - -**Always list the files in a directory before creating new files.** - -**When creating a Next.js project, always wait 30 seconds after project creation.** - -**CRITICAL: Never use XML-style tool syntax like \`\` or \`\` tags. All tools are automatically available through MCP and will be called by the LLM when needed. Simply describe what you want to do in natural language.** - -**Web Search Best Practices:** -1. **Always search first**: Use search_web to find information before trying to fetch URLs -2. **Use reliable URLs**: Only fetch URLs from search results or known reliable domains -3. **Verify domains**: Stick to major sites like github.com, stackoverflow.com, docs sites, etc. -4. **Search workflow**: search_web → get reliable URLs → fetch_url with those URLs - -1. Clarify requirements and tech stack -2. Lookup technologies using Context7 tools OR web search tools -3. Retrieve current documentation and patterns -4. Scaffold or enhance projects under /tmp, maintaining clean structure -5. Follow framework and language conventions -6. Include error handling, testing, and CI/build scripts -7. Prioritize maintainability, readability, and DX (developer experience) - -**For Next.js projects:** -- Use \`create_nextjs_project\` tool to create new projects -- Use \`npm_run\` tool for npm commands like "run dev", "install", "build" -- Use filesystem tools to read/write files and list directories - ---- - -### ⚛️ NEXT.JS PROJECT RULES - -* **Use ONLY the App Router (/app)**, not the legacy Pages Router -* **Never create both (/app and /pages directories** -* **IMPORTANT: Always wait 30 seconds after creating a Next.js project before proceeding** -* Structure should include: - * app/layout.tsx – required root layout - * app/page.tsx - homepage - * app/about/page.tsx – nested routes - * components/, public/, etc. as needed - -If a Next.js project exists: - -* Validate it uses the App Router -* Extend or modify as needed based on the request - ---- - -### 🧪 TECH STACK (verify latest versions with Context7) - -**Frontend:** React, Next.js, Vue, Angular, Svelte -**Backend:** Node.js, Express, Fastify, NestJS, Koa -**Databases:** MongoDB, PostgreSQL, MySQL, SQLite, Redis -**Styling:** Tailwind CSS, CSS Modules, Styled Components -**Testing:** Jest, Vitest, Playwright, Cypress -**Build Tools:** Vite, Webpack, Rollup, Turbo -**Package Managers:** npm, yarn, pnpm + return `You are a NextJS development assistant. Create modern applications using current technologies. ---- +CORE RULES: +- Work only in /tmp directory +- Use Next.js App Router (/app), never Pages Router +- Wait 30 seconds after creating Next.js projects +- If project exists, enhance it - don't recreate +- Use Context7 tools for documentation lookup +- Tools called automatically - just describe what you need -### ✅ SUMMARY +AVAILABLE TOOLS: Context7 docs, web search, filesystem, npm commands, memory tools -* Always work in /tmp -* If a project exists, enhance it — don't recreate -* Always Use Context7 tools for everything: tech decisions, patterns, and examples -* Adhere to modern best practices in project setup, UI/UX, and code quality -`; +WORKFLOW: 1) Clarify requirements 2) Use Context7 for docs 3) Build in /tmp 4) Follow modern conventions`; } async initialize(): Promise { @@ -312,7 +160,8 @@ If a Next.js project exists: console.info( `🧠 Found ${memoryTools.length} memory management tools` ); - await this.loadStateFromMemory(); + // Skip slow memory restoration for faster startup + console.log('📥 Skipping memory restoration for faster startup'); } else { console.info( '⚠️ No memory tools available. State persistence disabled.' diff --git a/examples/mcp/docker-compose.yml b/examples/mcp/docker-compose.yml index d4a583c..1a520c0 100644 --- a/examples/mcp/docker-compose.yml +++ b/examples/mcp/docker-compose.yml @@ -48,12 +48,6 @@ services: condition: service_healthy networks: - inference-network - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:8080/health'] - interval: 30s - timeout: 10s - retries: 5 - start_period: 60s pull_policy: always restart: unless-stopped diff --git a/examples/mcp/mcp-servers/memory/index.js b/examples/mcp/mcp-servers/memory/index.js index 23a1a07..6d72042 100644 --- a/examples/mcp/mcp-servers/memory/index.js +++ b/examples/mcp/mcp-servers/memory/index.js @@ -12,8 +12,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/ import express from 'express'; import { randomUUID } from 'node:crypto'; import { z } from 'zod'; -import { promises as fs } from 'node:fs'; -import path from 'node:path'; +import MemoryManager from './memory-manager.js'; import { createMcpLogger, logMcpRequest, @@ -31,6 +30,9 @@ const transports = {}; const memoryDir = process.env.MEMORY_DIR || '/tmp/memory'; +// Initialize the memory manager +const memoryManager = new MemoryManager(memoryDir); + logger.info('MCP Memory Server initializing', { memoryDir, nodeVersion: process.version, @@ -38,33 +40,6 @@ logger.info('MCP Memory Server initializing', { environment: process.env.NODE_ENV || 'development', }); -/** - * Ensure memory directory exists - */ -async function ensureMemoryDir() { - try { - logger.debug('Ensuring memory directory exists', { memoryDir }); - await fs.mkdir(memoryDir, { recursive: true }); - logger.debug('Memory directory ready', { memoryDir }); - } catch (error) { - logger.error('Failed to create memory directory', { - memoryDir, - error: error.message, - stack: error.stack, - }); - throw error; - } -} - -/** - * Get memory file path for a given session - */ -function getMemoryPath(sessionId) { - const memoryPath = path.join(memoryDir, `${sessionId}.json`); - logger.debug('Generated memory path', { sessionId, memoryPath }); - return memoryPath; -} - /** * Create and configure the MCP server */ @@ -88,46 +63,21 @@ function createMcpServer() { async ({ sessionId, state, context }) => { const startTime = Date.now(); - logger.info('save-state tool called', { + logger.debug('save-state tool called', { sessionId, - hasContext: !!context, - stateKeys: Object.keys(state || {}), stateSize: JSON.stringify(state).length, }); try { - await ensureMemoryDir(); - - const memoryData = { - sessionId, - state, - context, - timestamp: new Date().toISOString(), - lastError: null, - }; - - logger.debug('Preparing memory data for save', { - sessionId, - dataSize: JSON.stringify(memoryData).length, - hasContext: !!context, - }); - - logger.info(`Saving state for session: ${sessionId}`, { - sessionId, - state: JSON.stringify(state), - context, - }); - - const memoryPath = getMemoryPath(sessionId); - await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); + // Use fast in-memory operation + await memoryManager.saveState(sessionId, state, context); const duration = Date.now() - startTime; logger.info('State saved successfully', { sessionId, - memoryPath, duration, - dataSize: JSON.stringify(memoryData).length, + dataSize: JSON.stringify(state).length, }); logMcpToolCall(logger, 'save-state', sessionId, { sessionId }); @@ -195,33 +145,13 @@ function createMcpServer() { }); try { - await ensureMemoryDir(); - - const memoryData = { - sessionId, - state, - context, - timestamp: new Date().toISOString(), - lastError: { - ...error, - timestamp: new Date().toISOString(), - }, - }; - - logger.debug('Preparing error memory data for save', { - sessionId, - dataSize: JSON.stringify(memoryData).length, - errorType: error.code || 'unknown', - }); - - const memoryPath = getMemoryPath(sessionId); - await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); + // Use fast in-memory operation with error tracking + await memoryManager.saveErrorState(sessionId, state, error, context); const duration = Date.now() - startTime; logger.info('Error state saved successfully', { sessionId, - memoryPath, duration, errorMessage: error.message, }); @@ -277,94 +207,64 @@ function createMcpServer() { async ({ sessionId }) => { const startTime = Date.now(); - logger.info('restore-state tool called', { sessionId }); + logger.debug('restore-state tool called', { sessionId }); try { - const memoryPath = getMemoryPath(sessionId); - - logger.debug('Attempting to read memory file', { - sessionId, - memoryPath, - }); + // Use fast in-memory operation + const memoryData = await memoryManager.restoreState(sessionId); - try { - const fileContent = await fs.readFile(memoryPath, 'utf8'); - const memoryData = JSON.parse(fileContent); - - const duration = Date.now() - startTime; + const duration = Date.now() - startTime; - logger.info('State restored successfully', { + if (!memoryData) { + logger.debug('No saved state found for session', { sessionId, - hasError: !!memoryData.lastError, - timestamp: memoryData.timestamp, - contextPresent: !!memoryData.context, duration, - fileSize: fileContent.length, }); - logMcpToolCall(logger, 'restore-state', sessionId, { sessionId }); - return { content: [ { type: 'text', - text: JSON.stringify( - { - sessionId: memoryData.sessionId, - state: memoryData.state, - context: memoryData.context, - timestamp: memoryData.timestamp, - lastError: memoryData.lastError, - hasError: !!memoryData.lastError, - }, - null, - 2 - ), + text: `No saved state found for session: ${sessionId}`, }, ], }; - } catch (readError) { - if (readError.code === 'ENOENT') { - const duration = Date.now() - startTime; - - logger.info('No saved state found for session', { - sessionId, - duration, - }); - logMcpToolCall(logger, 'restore-state', sessionId, { sessionId }); - - return { - content: [ - { - type: 'text', - text: `No saved state found for session: ${sessionId}`, - }, - ], - }; - } + } - logger.error('Failed to read memory file', { - sessionId, - memoryPath, - error: readError.message, - code: readError.code, - }); + logger.debug('State restored successfully', { + sessionId, + hasError: !!memoryData.lastError, + duration, + }); - throw readError; - } + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + sessionId: memoryData.sessionId, + state: memoryData.state, + context: memoryData.context, + timestamp: memoryData.timestamp, + lastError: memoryData.lastError, + hasError: !!memoryData.lastError, + }, + null, + 2 + ), + }, + ], + }; } catch (error) { const duration = Date.now() - startTime; logger.error('Failed to restore state', { sessionId, error: error.message, - stack: error.stack, duration, }); - logMcpError('restore-state', error, { sessionId }); - logMcpToolCall('restore-state', { sessionId }, false, duration); - return { content: [ { @@ -384,57 +284,12 @@ function createMcpServer() { logger.info('list-sessions tool called'); try { - await ensureMemoryDir(); - const files = await fs.readdir(memoryDir); - const jsonFiles = files.filter((file) => file.endsWith('.json')); - - logger.debug('Found session files', { - totalFiles: files.length, - jsonFiles: jsonFiles.length, - files: jsonFiles, - }); - - const sessions = []; - let successCount = 0; - let errorCount = 0; - - for (const file of jsonFiles) { - try { - const sessionId = path.basename(file, '.json'); - const filePath = path.join(memoryDir, file); - const memoryData = JSON.parse(await fs.readFile(filePath, 'utf8')); - - sessions.push({ - sessionId, - context: memoryData.context, - timestamp: memoryData.timestamp, - hasError: !!memoryData.lastError, - lastError: memoryData.lastError?.message, - }); - - successCount++; - - logger.debug('Session file processed successfully', { - sessionId, - file, - hasError: !!memoryData.lastError, - }); - } catch (readError) { - errorCount++; - - logger.warn('Failed to read session file', { - file, - error: readError.message, - }); - } - } - + // Use fast in-memory operation + const sessions = memoryManager.listSessions(); const duration = Date.now() - startTime; logger.info('Sessions listed successfully', { totalSessions: sessions.length, - successCount, - errorCount, duration, }); @@ -503,48 +358,22 @@ function createMcpServer() { }); try { - await ensureMemoryDir(); - const timestampedMessages = messages.map((msg) => ({ ...msg, timestamp: msg.timestamp || new Date().toISOString(), })); - let existingData = {}; - const memoryPath = getMemoryPath(sessionId); - try { - const fileContent = await fs.readFile(memoryPath, 'utf8'); - existingData = JSON.parse(fileContent); - } catch { - logger.debug('No existing memory file found, creating new one', { - sessionId, - }); - } - - const memoryData = { - ...existingData, + // Use fast in-memory operation + await memoryManager.saveConversation( sessionId, - conversation: { - messages: timestampedMessages, - context, - lastUpdated: new Date().toISOString(), - }, - timestamp: new Date().toISOString(), - }; - - logger.debug('Preparing conversation data for save', { - sessionId, - dataSize: JSON.stringify(memoryData).length, - messageCount: timestampedMessages.length, - }); - - await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); + timestampedMessages, + context + ); const duration = Date.now() - startTime; logger.info('Conversation saved successfully', { sessionId, - memoryPath, duration, messageCount: timestampedMessages.length, }); @@ -607,8 +436,6 @@ function createMcpServer() { }); try { - await ensureMemoryDir(); - const messageTimestamp = timestamp || new Date().toISOString(); const newMessage = { role, @@ -616,44 +443,11 @@ function createMcpServer() { timestamp: messageTimestamp, }; - // Load existing memory data - let memoryData = {}; - const memoryPath = getMemoryPath(sessionId); - try { - const fileContent = await fs.readFile(memoryPath, 'utf8'); - memoryData = JSON.parse(fileContent); - } catch { - // File doesn't exist yet, create base structure - logger.debug('No existing memory file found, creating new one', { - sessionId, - }); - memoryData = { - sessionId, - timestamp: new Date().toISOString(), - }; - } - - // Initialize conversation structure if it doesn't exist - if (!memoryData.conversation) { - memoryData.conversation = { - messages: [], - context: null, - lastUpdated: new Date().toISOString(), - }; - } - - // Add the new message - memoryData.conversation.messages.push(newMessage); - memoryData.conversation.lastUpdated = new Date().toISOString(); - memoryData.timestamp = new Date().toISOString(); - - logger.debug('Preparing message data for save', { + // Use fast in-memory operation + const totalMessages = await memoryManager.addMessage( sessionId, - role, - totalMessages: memoryData.conversation.messages.length, - }); - - await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); + newMessage + ); const duration = Date.now() - startTime; @@ -661,7 +455,7 @@ function createMcpServer() { sessionId, role, duration, - totalMessages: memoryData.conversation.messages.length, + totalMessages, }); logMcpToolCall(logger, 'add-message', sessionId, { sessionId, role }); @@ -670,7 +464,7 @@ function createMcpServer() { content: [ { type: 'text', - text: `Message added successfully for session: ${sessionId}. Role: ${role}. Total messages: ${memoryData.conversation.messages.length}`, + text: `Message added successfully for session: ${sessionId}. Role: ${role}. Total messages: ${totalMessages}`, }, ], }; @@ -726,117 +520,76 @@ function createMcpServer() { }); try { - const memoryPath = getMemoryPath(sessionId); - - logger.debug('Attempting to read memory file for conversation', { - sessionId, - memoryPath, - }); - - try { - const fileContent = await fs.readFile(memoryPath, 'utf8'); - const memoryData = JSON.parse(fileContent); - - if (!memoryData.conversation || !memoryData.conversation.messages) { - const duration = Date.now() - startTime; - - logger.info('No conversation found for session', { - sessionId, - duration, - }); - - logMcpToolCall(logger, 'get-conversation', sessionId, { - sessionId, - }); - - return { - content: [ - { - type: 'text', - text: `No conversation found for session: ${sessionId}`, - }, - ], - }; - } - - let messages = [...memoryData.conversation.messages]; - - // Apply role filter if specified - if (filterRole) { - messages = messages.filter((msg) => msg.role === filterRole); - } - - // Apply limit if specified (get most recent messages) - if (limit && limit > 0) { - messages = messages.slice(-limit); - } + // Use fast in-memory operation + const conversationData = await memoryManager.getConversation(sessionId); - const duration = Date.now() - startTime; + const duration = Date.now() - startTime; - logger.info('Conversation retrieved successfully', { + if (!conversationData || !conversationData.messages) { + logger.info('No conversation found for session', { sessionId, - totalMessages: memoryData.conversation.messages.length, - filteredMessages: messages.length, - filterRole, - limit, duration, }); - logMcpToolCall(logger, 'get-conversation', sessionId, { sessionId }); + logMcpToolCall(logger, 'get-conversation', sessionId, { + sessionId, + }); return { content: [ { type: 'text', - text: JSON.stringify( - { - sessionId: memoryData.sessionId, - conversation: { - messages, - context: memoryData.conversation.context, - lastUpdated: memoryData.conversation.lastUpdated, - totalMessages: memoryData.conversation.messages.length, - filteredMessages: messages.length, - }, - timestamp: memoryData.timestamp, - }, - null, - 2 - ), + text: `No conversation found for session: ${sessionId}`, }, ], }; - } catch (readError) { - if (readError.code === 'ENOENT') { - const duration = Date.now() - startTime; - - logger.info('No memory file found for session', { - sessionId, - duration, - }); - logMcpToolCall(logger, 'get-conversation', sessionId, { - sessionId, - }); - - return { - content: [ - { - type: 'text', - text: `No conversation found for session: ${sessionId}`, - }, - ], - }; - } + } - logger.error('Failed to read memory file for conversation', { - sessionId, - memoryPath, - error: readError.message, - code: readError.code, - }); + let messages = [...conversationData.messages]; + + // Apply role filter if specified + if (filterRole) { + messages = messages.filter((msg) => msg.role === filterRole); + } - throw readError; + // Apply limit if specified (get most recent messages) + if (limit && limit > 0) { + messages = messages.slice(-limit); } + + logger.info('Conversation retrieved successfully', { + sessionId, + totalMessages: conversationData.messages.length, + filteredMessages: messages.length, + filterRole, + limit, + duration, + }); + + logMcpToolCall(logger, 'get-conversation', sessionId, { sessionId }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + sessionId, + conversation: { + messages, + context: conversationData.context, + lastUpdated: conversationData.lastUpdated, + totalMessages: conversationData.messages.length, + filteredMessages: messages.length, + }, + timestamp: conversationData.timestamp, + }, + null, + 2 + ), + }, + ], + }; } catch (error) { const duration = Date.now() - startTime; @@ -884,109 +637,59 @@ function createMcpServer() { }); try { - const memoryPath = getMemoryPath(sessionId); - - if (keepOtherData) { - // Load existing data and only clear conversation - try { - const fileContent = await fs.readFile(memoryPath, 'utf8'); - const memoryData = JSON.parse(fileContent); - - // Clear conversation but keep other data - delete memoryData.conversation; - memoryData.timestamp = new Date().toISOString(); + // Use fast in-memory operation + const cleared = await memoryManager.clearConversation( + sessionId, + keepOtherData + ); - await fs.writeFile(memoryPath, JSON.stringify(memoryData, null, 2)); + const duration = Date.now() - startTime; - const duration = Date.now() - startTime; + if (!cleared) { + logger.info('No conversation found to clear', { + sessionId, + duration, + }); + logMcpToolCall(logger, 'clear-conversation', sessionId, { + sessionId, + }); - logger.info( - 'Conversation cleared successfully (keeping other data)', + return { + content: [ { - sessionId, - duration, - } - ); + type: 'text', + text: `No conversation found to clear for session: ${sessionId}`, + }, + ], + }; + } - logMcpToolCall(logger, 'clear-conversation', sessionId, { - sessionId, - }); + const message = keepOtherData + ? `Conversation cleared successfully for session: ${sessionId} (other data preserved)` + : `Entire session cleared successfully: ${sessionId}`; - return { - content: [ - { - type: 'text', - text: `Conversation cleared successfully for session: ${sessionId} (other data preserved)`, - }, - ], - }; - } catch (readError) { - if (readError.code === 'ENOENT') { - const duration = Date.now() - startTime; - - logger.info('No conversation found to clear', { - sessionId, - duration, - }); - logMcpToolCall(logger, 'clear-conversation', sessionId, { - sessionId, - }); - - return { - content: [ - { - type: 'text', - text: `No conversation found to clear for session: ${sessionId}`, - }, - ], - }; - } - throw readError; - } - } else { - // Clear entire session file - try { - await fs.unlink(memoryPath); - - const duration = Date.now() - startTime; - - logger.info('Entire session cleared successfully', { - sessionId, - duration, - }); - logMcpToolCall(logger, 'clear-conversation', sessionId, { - sessionId, - }); - - return { - content: [ - { - type: 'text', - text: `Entire session cleared successfully: ${sessionId}`, - }, - ], - }; - } catch (unlinkError) { - if (unlinkError.code === 'ENOENT') { - const duration = Date.now() - startTime; - - logger.info('No session found to clear', { sessionId, duration }); - logMcpToolCall(logger, 'clear-conversation', sessionId, { - sessionId, - }); - - return { - content: [ - { - type: 'text', - text: `No session found to clear: ${sessionId}`, - }, - ], - }; - } - throw unlinkError; + logger.info( + keepOtherData + ? 'Conversation cleared successfully (keeping other data)' + : 'Entire session cleared successfully', + { + sessionId, + duration, } - } + ); + + logMcpToolCall(logger, 'clear-conversation', sessionId, { + sessionId, + }); + + return { + content: [ + { + type: 'text', + text: message, + }, + ], + }; } catch (error) { const duration = Date.now() - startTime; @@ -994,7 +697,6 @@ function createMcpServer() { sessionId, keepOtherData, error: error.message, - code: error.code, stack: error.stack, duration, }); @@ -1026,16 +728,24 @@ function createMcpServer() { logger.info('clear-session tool called', { sessionId }); try { - const memoryPath = getMemoryPath(sessionId); + // Use fast in-memory operation + const cleared = await memoryManager.clearSession(sessionId); - logger.debug('Attempting to delete session file', { - sessionId, - memoryPath, - }); + const duration = Date.now() - startTime; - await fs.unlink(memoryPath); + if (!cleared) { + logger.info('No session found to clear', { sessionId, duration }); + logMcpToolCall('clear-session', { sessionId }, true, duration); - const duration = Date.now() - startTime; + return { + content: [ + { + type: 'text', + text: `No session found to clear: ${sessionId}`, + }, + ], + }; + } logger.info('Session cleared successfully', { sessionId, duration }); logMcpToolCall('clear-session', { sessionId }, true, duration); @@ -1051,24 +761,9 @@ function createMcpServer() { } catch (error) { const duration = Date.now() - startTime; - if (error.code === 'ENOENT') { - logger.info('No session found to clear', { sessionId, duration }); - logMcpToolCall('clear-session', { sessionId }, true, duration); - - return { - content: [ - { - type: 'text', - text: `No session found to clear: ${sessionId}`, - }, - ], - }; - } - logger.error('Failed to clear session', { sessionId, error: error.message, - code: error.code, stack: error.stack, duration, }); @@ -1258,17 +953,42 @@ logger.info('Starting MCP Memory Server', { environment: process.env.NODE_ENV || 'development', }); -app.listen(PORT, () => { - logger.info('MCP Memory Server started successfully', { - port: PORT, - memoryDir, - endpoints: ['/mcp', '/health'], - }); -}); +async function startServer() { + try { + await memoryManager.initialize(); + logger.info('MemoryManager initialized successfully'); + + app.listen(PORT, () => { + logger.info('MCP Memory Server started successfully', { + port: PORT, + memoryDir, + endpoints: ['/mcp', '/health'], + sessionsLoaded: memoryManager.getStats().sessionsInMemory, + }); + }); + } catch (error) { + logger.error('Failed to start server', { + error: error.message, + stack: error.stack, + }); + process.exit(1); + } +} -process.on('SIGTERM', () => { +startServer(); + +process.on('SIGTERM', async () => { logger.info('SIGTERM received, shutting down gracefully'); + try { + await memoryManager.shutdown(); + logger.info('MemoryManager shutdown completed'); + } catch (error) { + logger.error('Error during MemoryManager shutdown', { + error: error.message, + }); + } + Object.keys(transports).forEach((sessionId) => { logger.debug('Closing transport for session', { sessionId }); const transport = transports[sessionId]; @@ -1281,9 +1001,19 @@ process.on('SIGTERM', () => { process.exit(0); }); -process.on('SIGINT', () => { +process.on('SIGINT', async () => { logger.info('SIGINT received, shutting down gracefully'); + try { + // Shutdown MemoryManager first to persist data + await memoryManager.shutdown(); + logger.info('MemoryManager shutdown completed'); + } catch (error) { + logger.error('Error during MemoryManager shutdown', { + error: error.message, + }); + } + Object.keys(transports).forEach((sessionId) => { logger.debug('Closing transport for session', { sessionId }); const transport = transports[sessionId]; diff --git a/examples/mcp/mcp-servers/memory/memory-manager.js b/examples/mcp/mcp-servers/memory/memory-manager.js new file mode 100644 index 0000000..9a5e71c --- /dev/null +++ b/examples/mcp/mcp-servers/memory/memory-manager.js @@ -0,0 +1,529 @@ +/** + * In-Memory Storage Manager with Background Persistence + * + * This module provides fast in-memory operations with automatic background + * disk persistence to avoid blocking the main thread on file I/O. + */ + +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { createMcpLogger } from './logger.js'; + +const logger = createMcpLogger('memory-manager', '1.0.0'); + +class MemoryManager { + constructor(memoryDir = '/tmp/memory', persistIntervalMs = 5000) { + this.memoryDir = memoryDir; + this.persistIntervalMs = persistIntervalMs; + + this.memoryStore = new Map(); + + this.dirtySet = new Set(); + + this.persistenceInterval = null; + this.isShuttingDown = false; + + logger.info('MemoryManager initialized', { + memoryDir: this.memoryDir, + persistIntervalMs: this.persistIntervalMs, + }); + } + + /** + * Initialize the memory manager + */ + async initialize() { + try { + await fs.mkdir(this.memoryDir, { recursive: true }); + + await this.loadExistingSessions(); + + this.startBackgroundPersistence(); + + logger.info('MemoryManager started successfully', { + loadedSessions: this.memoryStore.size, + }); + } catch (error) { + logger.error('Failed to initialize MemoryManager', { + error: error.message, + stack: error.stack, + }); + throw error; + } + } + + /** + * Load existing sessions from disk into memory on startup + */ + async loadExistingSessions() { + try { + const files = await fs.readdir(this.memoryDir); + const jsonFiles = files.filter((file) => file.endsWith('.json')); + + logger.debug('Loading existing sessions', { + totalFiles: files.length, + jsonFiles: jsonFiles.length, + }); + + let loadedCount = 0; + let errorCount = 0; + + for (const file of jsonFiles) { + try { + const sessionId = path.basename(file, '.json'); + const filePath = path.join(this.memoryDir, file); + const fileContent = await fs.readFile(filePath, 'utf8'); + const memoryData = JSON.parse(fileContent); + + this.memoryStore.set(sessionId, memoryData); + loadedCount++; + + logger.debug('Session loaded successfully', { sessionId }); + } catch (readError) { + errorCount++; + logger.warn('Failed to load session file', { + file, + error: readError.message, + }); + } + } + + logger.info('Session loading completed', { + loadedCount, + errorCount, + totalSessions: this.memoryStore.size, + }); + } catch (error) { + logger.error('Failed to load existing sessions', { + error: error.message, + }); + // Don't throw - we can continue with empty memory + } + } + + /** + * Start background persistence process + */ + startBackgroundPersistence() { + if (this.persistenceInterval) { + globalThis.clearInterval(this.persistenceInterval); + } + + this.persistenceInterval = globalThis.setInterval(async () => { + if (this.isShuttingDown) return; + + logger.debug('Background persistence tick', { + dirtySessions: this.dirtySet.size, + totalSessions: this.memoryStore.size, + }); + + try { + await this.persistDirtySessions(); + } catch (error) { + logger.error('Background persistence failed', { + error: error.message, + }); + } + }, this.persistIntervalMs); + + logger.info('Background persistence started', { + intervalMs: this.persistIntervalMs, + intervalId: this.persistenceInterval, + }); + } + + /** + * Persist dirty sessions to disk + */ + async persistDirtySessions() { + if (this.dirtySet.size === 0) return; + + const sessionsToPersist = Array.from(this.dirtySet); + this.dirtySet.clear(); + + logger.debug('Persisting dirty sessions', { + sessionCount: sessionsToPersist.length, + }); + + const persistPromises = sessionsToPersist.map(async (sessionId) => { + try { + const memoryData = this.memoryStore.get(sessionId); + if (!memoryData) { + logger.warn('Session not found in memory during persistence', { + sessionId, + }); + return; + } + + const filePath = path.join(this.memoryDir, `${sessionId}.json`); + await fs.writeFile(filePath, JSON.stringify(memoryData, null, 2)); + + logger.debug('Session persisted successfully', { sessionId }); + } catch (error) { + logger.error('Failed to persist session', { + sessionId, + error: error.message, + }); + this.dirtySet.add(sessionId); + } + }); + + await Promise.allSettled(persistPromises); + } + + /** + * Save state to memory (fast, synchronous operation) + */ + saveState(sessionId, state, context = null) { + const memoryData = { + sessionId, + state, + context, + timestamp: new Date().toISOString(), + lastError: this.memoryStore.get(sessionId)?.lastError || null, + }; + + this.memoryStore.set(sessionId, memoryData); + this.dirtySet.add(sessionId); + + logger.debug('State saved to memory', { + sessionId, + stateSize: JSON.stringify(state).length, + totalDirtySessions: this.dirtySet.size, + }); + + return memoryData; + } + + /** + * Save error state for a session (fast in-memory operation) + */ + async saveErrorState(sessionId, state, error, context) { + const memoryData = { + sessionId, + state, + context, + timestamp: new Date().toISOString(), + lastError: { + ...error, + timestamp: new Date().toISOString(), + }, + }; + + this.memoryStore.set(sessionId, memoryData); + + this.dirtySet.add(sessionId); + + logger.debug('Error state saved to memory', { + sessionId, + errorMessage: error.message, + memorySize: this.memoryStore.size, + }); + } + + /** + * Restore state from memory (fast, synchronous operation) + */ + restoreState(sessionId) { + const memoryData = this.memoryStore.get(sessionId); + + if (!memoryData) { + logger.debug('No state found in memory', { sessionId }); + return null; + } + + logger.debug('State restored from memory', { + sessionId, + hasError: !!memoryData.lastError, + }); + + return memoryData; + } + + /** + * List all sessions in memory (fast, synchronous operation) + */ + listSessions() { + const sessions = Array.from(this.memoryStore.entries()).map( + ([sessionId, data]) => ({ + sessionId, + context: data.context, + timestamp: data.timestamp, + hasError: !!data.lastError, + lastError: data.lastError?.message, + }) + ); + + logger.debug('Sessions listed from memory', { + sessionCount: sessions.length, + }); + + return sessions; + } + + /** + * Clear session from memory and mark for deletion + */ + clearSession(sessionId) { + const existed = this.memoryStore.has(sessionId); + this.memoryStore.delete(sessionId); + this.dirtySet.delete(sessionId); + + if (existed) { + this.scheduleFileDeletion(sessionId); + logger.debug('Session cleared from memory', { sessionId }); + } + + return existed; + } + + /** + * Schedule file deletion for the next persistence cycle + */ + scheduleFileDeletion(sessionId) { + this.dirtySet.add(`__DELETE__${sessionId}`); + } + + /** + * Enhanced persistence that handles deletions + */ + async persistDirtySessionsWithDeletions() { + if (this.dirtySet.size === 0) return; + + const operations = Array.from(this.dirtySet); + this.dirtySet.clear(); + + const saveOperations = operations.filter( + (op) => !op.startsWith('__DELETE__') + ); + const deleteOperations = operations + .filter((op) => op.startsWith('__DELETE__')) + .map((op) => op.replace('__DELETE__', '')); + + logger.debug('Persisting sessions with operations', { + saveCount: saveOperations.length, + deleteCount: deleteOperations.length, + }); + + const savePromises = saveOperations.map(async (sessionId) => { + try { + const memoryData = this.memoryStore.get(sessionId); + if (!memoryData) return; + + const filePath = path.join(this.memoryDir, `${sessionId}.json`); + await fs.writeFile(filePath, JSON.stringify(memoryData, null, 2)); + } catch (error) { + logger.error('Failed to persist session', { + sessionId, + error: error.message, + }); + this.dirtySet.add(sessionId); + } + }); + + const deletePromises = deleteOperations.map(async (sessionId) => { + try { + const filePath = path.join(this.memoryDir, `${sessionId}.json`); + await fs.unlink(filePath); + logger.debug('Session file deleted', { sessionId }); + } catch (error) { + if (error.code !== 'ENOENT') { + logger.error('Failed to delete session file', { + sessionId, + error: error.message, + }); + } + } + }); + + await Promise.allSettled([...savePromises, ...deletePromises]); + } + + /** + * Graceful shutdown with immediate persistence + */ + async shutdown() { + logger.info('MemoryManager shutting down...'); + + this.isShuttingDown = true; + + if (this.persistenceInterval) { + globalThis.clearInterval(this.persistenceInterval); + this.persistenceInterval = null; + } + + try { + await this.persistDirtySessionsWithDeletions(); + logger.info('Final persistence completed successfully'); + } catch (error) { + logger.error('Failed to complete final persistence', { + error: error.message, + }); + } + + logger.info('MemoryManager shutdown completed'); + } + + /** + * Get memory statistics + */ + getStats() { + return { + sessionsInMemory: this.memoryStore.size, + dirtySessionsCount: this.dirtySet.size, + isShuttingDown: this.isShuttingDown, + persistIntervalMs: this.persistIntervalMs, + }; + } + + /** + * Save conversation to memory (fast in-memory operation) + */ + saveConversation(sessionId, messages, context = null) { + const existingData = this.memoryStore.get(sessionId) || {}; + + const timestampedMessages = messages.map((msg) => ({ + ...msg, + timestamp: msg.timestamp || new Date().toISOString(), + })); + + const memoryData = { + ...existingData, + sessionId, + conversation: { + messages: timestampedMessages, + context, + lastUpdated: new Date().toISOString(), + }, + timestamp: new Date().toISOString(), + }; + + this.memoryStore.set(sessionId, memoryData); + this.dirtySet.add(sessionId); + + logger.debug('Conversation saved to memory', { + sessionId, + messageCount: timestampedMessages.length, + memorySize: this.memoryStore.size, + }); + + return memoryData; + } + + /** + * Add message to conversation (fast in-memory operation) + */ + addMessage(sessionId, role, content, timestamp = null) { + const messageTimestamp = timestamp || new Date().toISOString(); + const newMessage = { + role, + content, + timestamp: messageTimestamp, + }; + + let memoryData = this.memoryStore.get(sessionId) || { + sessionId, + timestamp: new Date().toISOString(), + }; + + if (!memoryData.conversation) { + memoryData.conversation = { + messages: [], + context: null, + lastUpdated: new Date().toISOString(), + }; + } + + memoryData.conversation.messages.push(newMessage); + memoryData.conversation.lastUpdated = new Date().toISOString(); + memoryData.timestamp = new Date().toISOString(); + + this.memoryStore.set(sessionId, memoryData); + this.dirtySet.add(sessionId); + + logger.debug('Message added to memory', { + sessionId, + role, + totalMessages: memoryData.conversation.messages.length, + }); + + return memoryData; + } + + /** + * Get conversation from memory (fast, synchronous operation) + */ + getConversation(sessionId, filterRole = null, limit = null) { + const memoryData = this.memoryStore.get(sessionId); + + if ( + !memoryData || + !memoryData.conversation || + !memoryData.conversation.messages + ) { + logger.debug('No conversation found in memory', { sessionId }); + return null; + } + + let messages = [...memoryData.conversation.messages]; + + if (filterRole) { + messages = messages.filter((msg) => msg.role === filterRole); + } + + if (limit && limit > 0) { + messages = messages.slice(-limit); + } + + logger.debug('Conversation retrieved from memory', { + sessionId, + totalMessages: memoryData.conversation.messages.length, + filteredMessages: messages.length, + filterRole, + limit, + }); + + return { + sessionId: memoryData.sessionId, + conversation: { + messages, + context: memoryData.conversation.context, + lastUpdated: memoryData.conversation.lastUpdated, + totalMessages: memoryData.conversation.messages.length, + filteredMessages: messages.length, + }, + timestamp: memoryData.timestamp, + }; + } + + /** + * Clear conversation from memory while keeping other data + */ + clearConversation(sessionId, keepOtherData = true) { + if (keepOtherData) { + const memoryData = this.memoryStore.get(sessionId); + if (!memoryData) { + logger.debug('No session found to clear conversation', { sessionId }); + return false; + } + + delete memoryData.conversation; + memoryData.timestamp = new Date().toISOString(); + + this.memoryStore.set(sessionId, memoryData); + this.dirtySet.add(sessionId); + + logger.debug('Conversation cleared from memory (keeping other data)', { + sessionId, + }); + return true; + } else { + return this.clearSession(sessionId); + } + } +} + +MemoryManager.prototype.persistDirtySessions = + MemoryManager.prototype.persistDirtySessionsWithDeletions; + +export default MemoryManager; From 91705e1b13d0c1f0b5d117f3cffc22daaa0a0550 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Mon, 2 Jun 2025 13:07:48 +0000 Subject: [PATCH 36/38] docs(examples): Add Brave Search server package and TypeScript configuration - Created package.json for the MCP Brave Search server with necessary dependencies and scripts. - Added tsconfig.json for TypeScript configuration, specifying compiler options and file inclusion/exclusion. Signed-off-by: Eden Reich --- examples/mcp/.env.example | 3 + examples/mcp/README.md | 7 +- examples/mcp/agents/kubernetes/index.ts | 28 +- examples/mcp/agents/marketing/.env.example | 0 examples/mcp/agents/marketing/Dockerfile | 10 + examples/mcp/agents/marketing/README.md | 191 ++ examples/mcp/agents/marketing/index.ts | 642 +++++ .../mcp/agents/marketing/package-lock.json | 886 ++++++ examples/mcp/agents/marketing/package.json | 30 + examples/mcp/agents/nextjs/index.ts | 18 +- examples/mcp/docker-compose-agents.yml | 11 + examples/mcp/docker-compose.yml | 135 +- .../mcp/mcp-servers/brave-search/README.md | 161 ++ .../mcp/mcp-servers/brave-search/index.ts | 736 +++++ .../mcp/mcp-servers/brave-search/logger.ts | 114 + .../brave-search/package-lock.json | 2472 +++++++++++++++++ .../mcp/mcp-servers/brave-search/package.json | 34 + .../mcp-servers/brave-search/tsconfig.json | 19 + 18 files changed, 5425 insertions(+), 72 deletions(-) create mode 100644 examples/mcp/agents/marketing/.env.example create mode 100644 examples/mcp/agents/marketing/Dockerfile create mode 100644 examples/mcp/agents/marketing/README.md create mode 100644 examples/mcp/agents/marketing/index.ts create mode 100644 examples/mcp/agents/marketing/package-lock.json create mode 100644 examples/mcp/agents/marketing/package.json create mode 100644 examples/mcp/mcp-servers/brave-search/README.md create mode 100644 examples/mcp/mcp-servers/brave-search/index.ts create mode 100644 examples/mcp/mcp-servers/brave-search/logger.ts create mode 100644 examples/mcp/mcp-servers/brave-search/package-lock.json create mode 100644 examples/mcp/mcp-servers/brave-search/package.json create mode 100644 examples/mcp/mcp-servers/brave-search/tsconfig.json diff --git a/examples/mcp/.env.example b/examples/mcp/.env.example index e757694..4fefbba 100644 --- a/examples/mcp/.env.example +++ b/examples/mcp/.env.example @@ -50,3 +50,6 @@ DEEPSEEK_API_KEY= # Chosen provider PROVIDER=deepseek LLM=deepseek-chat + +# Brave search +BRAVE_API_KEY= diff --git a/examples/mcp/README.md b/examples/mcp/README.md index 8130d10..bf40d81 100644 --- a/examples/mcp/README.md +++ b/examples/mcp/README.md @@ -16,9 +16,10 @@ On another terminal, you can run specific examples using the Inference Gateway: ```bash # Run interactive specialized agents -docker compose -f docker-compose-agents.yml run --rm -it nextjs-agent # 🤖 Next.js development agent -docker compose -f docker-compose-agents.yml run --rm -it vite-agent # ⚡ Vite application agent -docker compose -f docker-compose-agents.yml run --rm -it kubernetes-agent # ☸️ Kubernetes operations agent +docker compose -f docker-compose-agents.yml run --rm -it --build nextjs-agent # 🤖 Next.js development agent +docker compose -f docker-compose-agents.yml run --rm -it --build vite-agent # ⚡ Vite application agent +docker compose -f docker-compose-agents.yml run --rm -it --build kubernetes-agent # ☸️ Kubernetes operations agent +docker compose -f docker-compose-agents.yml run --rm -it --build marketing-agent # 📈 Marketing research agent ``` ## 🧠 Memory & Error Recovery diff --git a/examples/mcp/agents/kubernetes/index.ts b/examples/mcp/agents/kubernetes/index.ts index 04843e2..9563e8d 100644 --- a/examples/mcp/agents/kubernetes/index.ts +++ b/examples/mcp/agents/kubernetes/index.ts @@ -69,16 +69,17 @@ class KubernetesAgent { this.config = { client: new InferenceGatewayClient({ baseURL: 'http://inference-gateway:8080/v1', + timeout: 120000, }), provider: (process.env.PROVIDER as Provider) || Provider.groq, model: process.env.LLM || 'llama-3.3-70b-versatile', conversationHistory: [], - maxRetries: 3, - retryDelayMs: 10000, + maxRetries: 5, + retryDelayMs: 5000, iterationCount: 0, totalTokensUsed: 0, - maxTokensPerRequest: 3000, - maxHistoryLength: 10, + maxTokensPerRequest: 2500, + maxHistoryLength: 6, sessionId: process.env.SESSION_ID || randomUUID(), memoryEnabled: true, abortController: new globalThis.AbortController(), @@ -736,19 +737,32 @@ WORKFLOW: 1) Clarify requirements 2) Use Context7 for K8s docs 3) Create manifes } }, onError: async (error) => { - console.error(`\n❌ Stream Error: ${error.error}`); + // Check if this is a "terminated" or stream failure error + const errorMsg = error.error || ''; + if ( + errorMsg.includes('terminated') || + errorMsg.includes('stream') || + errorMsg.includes('context canceled') || + errorMsg.includes('upstream provider error') + ) { + // This will be handled by the retry logic in the main loop + console.error(`\n❌ Stream Error (retryable): ${errorMsg}`); + throw new Error(`Retryable stream error: ${errorMsg}`); + } + + console.error(`\n❌ Stream Error: ${errorMsg}`); // Record the error with detailed context await this.recordError( 'stream_error', - error.error || 'Unknown stream error', + errorMsg || 'Unknown stream error', `Stream error during iteration ${this.config.iterationCount}. Last tool call: ${this.config.lastFailedToolCall?.name || 'none'}`, this.config.lastFailedToolCall, userInput ); throw new Error( - `Stream error: ${error.error || 'Unknown stream error'}` + `Stream error: ${errorMsg || 'Unknown stream error'}` ); }, onFinish: async () => { diff --git a/examples/mcp/agents/marketing/.env.example b/examples/mcp/agents/marketing/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/examples/mcp/agents/marketing/Dockerfile b/examples/mcp/agents/marketing/Dockerfile new file mode 100644 index 0000000..4034602 --- /dev/null +++ b/examples/mcp/agents/marketing/Dockerfile @@ -0,0 +1,10 @@ +FROM node:20-alpine + +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +EXPOSE 3005 +ENV HOST=0.0.0.0 + +CMD ["npm", "start"] diff --git a/examples/mcp/agents/marketing/README.md b/examples/mcp/agents/marketing/README.md new file mode 100644 index 0000000..dc23972 --- /dev/null +++ b/examples/mcp/agents/marketing/README.md @@ -0,0 +1,191 @@ +# Marketing Agent with Brave Search + +This example demonstrates how to use the Marketing Intelligence Agent with Brave Search capabilities for comprehensive market research and competitive analysis. + +## Setup + +1. **Get a Brave Search API Key** + + - Visit [Brave Search API](https://api.search.brave.com/) + - Sign up and get your API key + - Add it to your `.env` file: + + ```bash + BRAVE_API_KEY=your_brave_api_key_here + ``` + +2. **Set up environment variables** + + ```bash + # Copy the example environment file + cp .env.example .env + + # Edit .env and add your API keys: + BRAVE_API_KEY=your_brave_api_key_here + OPENAI_API_KEY=your_openai_key_here # Optional + GROQ_API_KEY=your_groq_key_here # Optional + ANTHROPIC_API_KEY=your_anthropic_key_here # Optional + ``` + +3. **Start the services** + + ```bash + docker-compose up --build + ``` + +4. **Run the marketing agent** + ```bash + # In a new terminal + cd agents/marketing + npm install + npm start + ``` + +## Features + +### 🔍 Brave Search Integration + +- **Web Search**: Comprehensive market research using `brave_web_search` +- **News Search**: Real-time news monitoring with `brave_news_search` +- **Marketing Research**: Automated competitive analysis with `marketing_research` + +### 📊 Marketing Capabilities + +- Market trend analysis +- Competitive intelligence +- Brand monitoring +- Content research +- Sentiment tracking +- Strategic recommendations + +### 🧠 Advanced Features + +- Memory persistence for research sessions +- Report generation and file saving +- Context7 integration for best practices +- Error recovery and retry mechanisms + +## Example Queries + +Here are some example queries you can try with the marketing agent: + +### Market Research + +``` +Research the electric vehicle market trends for 2024 +``` + +### Competitive Analysis + +``` +Compare Tesla vs BMW in the luxury EV segment +``` + +### Brand Monitoring + +``` +Find recent news and sentiment about Apple's latest iPhone launch +``` + +### Content Strategy + +``` +What are the top marketing trends for SaaS companies in 2024? +``` + +### Industry Analysis + +``` +Research the AI startup ecosystem and recent funding rounds +``` + +## Tool Examples + +### Using brave_web_search + +```json +{ + "query": "artificial intelligence marketing trends 2024", + "count": 15, + "country": "US", + "safesearch": "moderate", + "freshness": "pw" +} +``` + +### Using brave_news_search + +```json +{ + "query": "tech startup funding", + "count": 10, + "country": "US", + "freshness": "pd" +} +``` + +### Using marketing_research + +```json +{ + "brand": "Tesla", + "competitors": ["BMW", "Mercedes", "Audi"], + "topics": ["reviews", "pricing", "features", "sustainability"], + "country": "US" +} +``` + +## Expected Workflow + +1. **Initialize**: The agent starts and checks available tools +2. **Research**: User provides a marketing research objective +3. **Analysis**: Agent uses Brave Search to gather comprehensive data +4. **Insights**: Agent provides structured analysis and recommendations +5. **Persistence**: Results are saved to memory and optionally to files + +## Troubleshooting + +### Brave Search API Issues + +- Ensure your API key is valid and not expired +- Check API rate limits if getting 429 errors +- Verify the BRAVE_API_KEY environment variable is set + +### Docker Issues + +```bash +# Rebuild if there are issues +docker-compose down +docker-compose up --build + +# Check logs +docker-compose logs mcp-brave-search +``` + +### Agent Connection Issues + +```bash +# Verify services are running +docker-compose ps + +# Check gateway health +curl http://localhost:8080/health +``` + +## API Documentation + +The marketing agent uses the official `mcp/brave-search` Docker image which provides: + +- **brave_web_search**: Web search with filtering options +- **brave_news_search**: News search with recency filters +- **marketing_research**: Automated competitive research + +For detailed API documentation, see the [Brave Search API docs](https://api.search.brave.com/app/documentation/web-search/get-started). + +## Best Practices + +1. **Specific Queries**: Use targeted search terms for better results +2. **Country Filtering**: Specify regions for localized insights +3. **Freshness Filters**: Use recent data for trend analysis +4. **Batch Research**: Combine multiple search types for comprehensive analysis +5. **Save Results**: Use memory tools to persist valuable research diff --git a/examples/mcp/agents/marketing/index.ts b/examples/mcp/agents/marketing/index.ts new file mode 100644 index 0000000..729407b --- /dev/null +++ b/examples/mcp/agents/marketing/index.ts @@ -0,0 +1,642 @@ +/** + * Interactive Marketing Agent + * + * This agent specializes in marketing research, competitive analysis, + * brand monitoring, and market intelligence using Brave Search and other MCP tools. + */ + +import { + InferenceGatewayClient, + MessageRole, + Provider, + SchemaChatCompletionMessageToolCall, +} from '@inference-gateway/sdk'; +import { randomUUID } from 'crypto'; +import * as dotenv from 'dotenv'; +import * as path from 'path'; +import * as readline from 'readline'; + +dotenv.config({ path: path.join(__dirname, '.env') }); + +interface ErrorRecord { + timestamp: string; + iterationCount: number; + errorType: 'stream_error' | 'tool_error' | 'mcp_error' | 'memory_error'; + errorMessage: string; + context: string; + toolName?: string; + toolId?: string; + toolArguments?: any; + userInput?: string; + recoveryAttempted: boolean; +} + +interface AgentConfig { + client: InferenceGatewayClient; + provider: Provider; + model: string; + conversationHistory: Array<{ role: MessageRole; content: string }>; + maxRetries: number; + retryDelayMs: number; + iterationCount: number; + totalTokensUsed: number; + maxTokensPerRequest: number; + maxHistoryLength: number; + sessionId: string; + memoryEnabled: boolean; + abortController: globalThis.AbortController; + errorHistory: ErrorRecord[]; + lastFailedToolCall?: { + name: string; + id: string; + arguments: any; + timestamp: string; + }; +} + +class MarketingAgent { + private config: AgentConfig; + private rl: readline.Interface; + + constructor() { + console.log('🔧 Debug - Environment variables:'); + console.log(' PROVIDER:', process.env.PROVIDER); + console.log(' LLM:', process.env.LLM); + + this.config = { + client: new InferenceGatewayClient({ + baseURL: 'http://inference-gateway:8080/v1', + timeout: 120000, + }), + provider: (process.env.PROVIDER as Provider) || Provider.groq, + model: process.env.LLM || 'llama-3.3-70b-versatile', + conversationHistory: [], + maxRetries: 5, + retryDelayMs: 5000, + iterationCount: 0, + totalTokensUsed: 0, + maxTokensPerRequest: 2500, + maxHistoryLength: 6, + sessionId: process.env.SESSION_ID || randomUUID(), + memoryEnabled: true, + abortController: new globalThis.AbortController(), + errorHistory: [], + lastFailedToolCall: undefined, + }; + + this.rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + this.config.conversationHistory.push({ + role: MessageRole.system, + content: this.getSystemPrompt(), + }); + } + + private async delay(ms: number): Promise { + return new Promise((resolve, reject) => { + if (this.config.abortController.signal.aborted) { + reject(new Error('Aborted')); + return; + } + + const timeout = global.setTimeout(() => resolve(), ms); + + const abortHandler = () => { + global.clearTimeout(timeout); + reject(new Error('Aborted')); + }; + + this.config.abortController.signal.addEventListener( + 'abort', + abortHandler, + { once: true } + ); + }); + } + + /** + * Record an error to both local history and memory + */ + private async recordError( + errorType: ErrorRecord['errorType'], + errorMessage: string, + context: string, + toolCall?: any, + userInput?: string + ): Promise { + const errorRecord: ErrorRecord = { + timestamp: new Date().toISOString(), + iterationCount: this.config.iterationCount, + errorType, + errorMessage, + context, + toolName: toolCall?.function?.name, + toolId: toolCall?.id, + toolArguments: toolCall?.function?.arguments + ? JSON.parse(toolCall.function.arguments) + : undefined, + userInput: userInput?.substring(0, 100), + recoveryAttempted: false, + }; + + this.config.errorHistory.push(errorRecord); + + // Keep only last 10 errors to prevent memory bloat + if (this.config.errorHistory.length > 10) { + this.config.errorHistory = this.config.errorHistory.slice(-10); + } + + console.log(`📝 Recording ${errorType}: ${errorMessage}`); + + // Save to memory if enabled + if (this.config.memoryEnabled) { + await this.saveErrorToMemory(errorRecord); + } + } + + /** + * Save error state to memory with detailed context + */ + private async saveErrorToMemory(errorRecord: ErrorRecord): Promise { + if (!this.config.memoryEnabled) return; + + try { + const errorState = { + conversationHistory: this.config.conversationHistory.slice(-3), + iterationCount: this.config.iterationCount, + totalTokensUsed: this.config.totalTokensUsed, + timestamp: new Date().toISOString(), + errorHistory: this.config.errorHistory, + lastError: errorRecord, + }; + + console.log( + `🚨 Saving error state to memory for session: ${this.config.sessionId}` + ); + + await this.config.client.streamChatCompletion( + { + model: this.config.model, + messages: [ + { + role: MessageRole.system, + content: `You are a memory manager handling error recovery. You MUST call the save-error-state tool immediately with the error details below. + +ERROR DETAILS: +- Type: ${errorRecord.errorType} +- Message: ${errorRecord.errorMessage} +- Context: ${errorRecord.context} +- Tool: ${errorRecord.toolName || 'none'} +- Timestamp: ${errorRecord.timestamp} + +CRITICAL: Call save-error-state tool NOW with this data.`, + }, + { + role: MessageRole.user, + content: `Save error state for session ${this.config.sessionId}: ${JSON.stringify(errorState)}`, + }, + ], + max_tokens: this.config.maxTokensPerRequest, + }, + { + onChunk: (chunk) => { + if (chunk.choices?.[0]?.delta?.content) { + process.stdout.write(chunk.choices[0].delta.content); + } + }, + onError: (error) => { + console.error(`❌ Memory save error: ${error}`); + }, + }, + this.config.provider, + this.config.abortController.signal + ); + + console.log('💾 Error state saved to memory system'); + } catch (memoryError) { + console.log( + `⚠️ Failed to save error to memory: ${(memoryError as Error).message}` + ); + } + } + + private getSystemPrompt(): string { + let errorHistoryPrompt = ''; + + if (this.config.errorHistory.length > 0) { + const recentErrors = this.config.errorHistory.slice(-2); + errorHistoryPrompt = `\nERROR CONTEXT: Previous ${recentErrors.length} errors encountered. Adapt approach accordingly.\n`; + } + + return `You are a Marketing Intelligence Agent specializing in market research, competitive analysis, brand monitoring, and marketing strategy development.${errorHistoryPrompt} + +CORE CAPABILITIES: +- Market research and trend analysis using Brave Search +- Competitive intelligence and brand monitoring +- Content research and marketing insights +- News monitoring and sentiment tracking +- Marketing strategy recommendations + +AVAILABLE TOOLS: +- brave_web_search: Comprehensive web search for market research +- brave_news_search: Real-time news search for trend monitoring +- marketing_research: Automated competitive analysis and brand research +- Context7 tools: Latest marketing best practices and documentation +- Memory tools: Session persistence and research history +- Filesystem tools: Save research reports and analysis + +WORKFLOW GUIDELINES: +1. Always start by understanding the marketing objective or research goal +2. Use Brave Search tools for comprehensive market intelligence +3. Cross-reference findings with news search for current trends +4. Save important research to memory for future reference +5. Provide actionable insights and strategic recommendations + +SEARCH STRATEGY: +- Use specific, targeted queries for better results +- Include competitors in research for comparative analysis +- Filter by country/region when relevant for localized insights +- Use freshness filters to get the most current information +- Combine web and news search for comprehensive coverage + +Always provide structured, actionable marketing insights with clear next steps.`; + } + + async initialize(): Promise { + console.log( + `📈 Marketing Intelligence Agent initialized using ${this.config.model} on ${this.config.provider}\n` + ); + + let attempt = 0; + while (attempt < this.config.maxRetries) { + try { + // Health check + const isHealthy = await this.config.client.healthCheck(); + if (!isHealthy) { + console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); + process.exit(1); + } + + const tools = await this.config.client.listTools(); + + // Check for Brave Search tools + const braveSearchTools = tools.data.filter((tool) => + [ + 'brave_web_search', + 'brave_news_search', + 'marketing_research', + ].includes(tool.name) + ); + + const context7Tools = tools.data.filter((tool) => + [ + 'c41_resolve-library-id', + 'c41_get-library-docs', + 'search_libraries', + 'get_library_details', + 'get_documentation', + ].includes(tool.name) + ); + + const memoryTools = tools.data.filter((tool) => + ['save-state', 'restore-state', 'list-sessions'].includes(tool.name) + ); + + const filesystemTools = tools.data.filter((tool) => + ['write_file', 'read_file', 'list_directory'].includes(tool.name) + ); + + if (braveSearchTools.length === 0) { + console.error( + '⚠️ No Brave Search tools available. Make sure the Brave Search MCP server is running.' + ); + console.error( + ' Required: brave_web_search, brave_news_search, marketing_research' + ); + console.error(' Run: docker-compose up --build'); + process.exit(1); + } + + console.info(`🔍 Found ${braveSearchTools.length} Brave Search tools:`); + braveSearchTools.forEach((tool, index) => { + console.info(` ${index + 1}. ${tool.name} - ${tool.description}`); + }); + + if (context7Tools.length > 0) { + console.info( + `📚 Found ${context7Tools.length} Context7 documentation tools` + ); + } + + if (memoryTools.length > 0) { + console.info( + `🧠 Found ${memoryTools.length} memory management tools` + ); + this.config.memoryEnabled = true; + console.log('📥 Memory persistence enabled'); + } else { + console.info( + '⚠️ No memory tools available. State persistence disabled.' + ); + this.config.memoryEnabled = false; + } + + if (filesystemTools.length > 0) { + console.info( + `📁 Found ${filesystemTools.length} filesystem tools for report saving` + ); + } + + console.info(''); + this.showWelcomeMessage(); + break; + } catch (error) { + attempt++; + console.error( + `❌ Initialization attempt ${attempt}/${this.config.maxRetries} failed:`, + (error as Error).message + ); + + if (attempt >= this.config.maxRetries) { + console.error('🚫 Max initialization attempts reached. Exiting.'); + process.exit(1); + } + + console.log(`⏳ Retrying in ${this.config.retryDelayMs}ms...`); + await this.delay(this.config.retryDelayMs); + } + } + } + + private showWelcomeMessage(): void { + console.log(` +╔═════════════════════════════════════════════════════════════════╗ +║ 📈 Marketing Intelligence Agent ║ +║ ║ +║ 🎯 Market Research & Competitive Analysis ║ +║ 📊 Brand Monitoring & Trend Analysis ║ +║ 📰 News Monitoring & Sentiment Tracking ║ +║ 💡 Marketing Strategy & Insights ║ +║ ║ +║ Commands: ║ +║ - Research [brand/topic] - Comprehensive market research ║ +║ - Analyze [brand] vs [competitor] - Competitive analysis ║ +║ - Monitor [brand] news - News and trend monitoring ║ +║ - Trends [industry] - Industry trend analysis ║ +║ - Help - Show available commands and examples ║ +║ - Exit/Quit - Quit the agent ║ +║ ║ +║ 🔍 Powered by Brave Search API for comprehensive market intel ║ +╚═════════════════════════════════════════════════════════════════╝ + +Examples: +• "Research Tesla's market position and customer sentiment" +• "Analyze Apple vs Samsung smartphone market share" +• "Monitor OpenAI news and competitor responses" +• "Trends in sustainable fashion industry" + +Ready for your marketing research request! 🚀 +`); + } + + private trimConversationHistory(): void { + if (this.config.conversationHistory.length > this.config.maxHistoryLength) { + const systemMessage = this.config.conversationHistory[0]; + const recentMessages = this.config.conversationHistory.slice( + -(this.config.maxHistoryLength - 1) + ); + this.config.conversationHistory = [systemMessage, ...recentMessages]; + console.log( + '🔄 Trimmed conversation history to prevent context overflow' + ); + } + } + + async processUserInput(userInput: string): Promise { + if (this.config.abortController.signal.aborted) { + console.log('🛑 Operation aborted by user'); + return; + } + + this.config.iterationCount++; + console.log(`\n--- Iteration ${this.config.iterationCount} ---`); + + // Handle special commands + const normalizedInput = userInput.toLowerCase().trim(); + if (normalizedInput === 'exit' || normalizedInput === 'quit') { + this.shutdown(); + return; + } + + if (normalizedInput === 'help') { + this.showHelpMessage(); + return; + } + + this.config.conversationHistory.push({ + role: MessageRole.user, + content: userInput, + }); + + this.trimConversationHistory(); + + let attempt = 0; + while (attempt < this.config.maxRetries) { + try { + let response = ''; + let toolCallResults: any[] = []; + let currentToolCall: any = null; + let assistantResponse = ''; + + console.log('🤔 Analyzing your marketing research request...\n'); + + await this.config.client.streamChatCompletion( + { + model: this.config.model, + messages: this.config.conversationHistory, + max_tokens: this.config.maxTokensPerRequest, + }, + { + onContent(content) { + process.stdout.write(content); + response += content; + }, + onMCPTool: async ( + toolCall: SchemaChatCompletionMessageToolCall + ) => { + console.log(`\n🛠️ Marketing Tool: ${toolCall.function.name}`); + try { + const args = JSON.parse(toolCall.function.arguments); + console.log(`📝 Arguments:`, JSON.stringify(args, null, 2)); + } catch { + console.log(`📝 Raw Arguments: ${toolCall.function.arguments}`); + } + console.log(`🔍 Tool ID: ${toolCall.id}\n`); + }, + onFinish: async () => { + console.log('\n\n✅ Development session completed!\n'); + console.log( + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' + ); + + if (assistantResponse.trim()) { + this.config.conversationHistory.push({ + role: MessageRole.assistant, + content: assistantResponse, + }); + } + }, + onError: async (error) => { + console.error(`❌ Stream error: ${error}`); + await this.recordError( + 'stream_error', + (error as Error).message, + 'Streaming chat completion failed', + currentToolCall, + userInput + ); + throw error; + }, + }, + this.config.provider, + this.config.abortController.signal + ); + + // Add assistant response to history + if (response.trim()) { + this.config.conversationHistory.push({ + role: MessageRole.assistant, + content: response, + }); + } + + // Add tool results to history + if (toolCallResults.length > 0) { + this.config.conversationHistory.push(...toolCallResults); + } + + console.log('\n'); + break; + } catch (error) { + attempt++; + console.error( + `❌ Attempt ${attempt}/${this.config.maxRetries} failed:`, + (error as Error).message + ); + + await this.recordError( + 'stream_error', + (error as Error).message, + `Processing attempt ${attempt} failed`, + undefined, + userInput + ); + + if (attempt >= this.config.maxRetries) { + console.error( + '🚫 Max retries reached. Please try a different approach.' + ); + return; + } + + console.log(`⏳ Retrying in ${this.config.retryDelayMs}ms...`); + await this.delay(this.config.retryDelayMs); + } + } + } + + private showHelpMessage(): void { + console.log(` +╔══════════════════════════════════════════════════════════════════════════════╗ +║ 📈 Marketing Intelligence Agent Help ║ +╠══════════════════════════════════════════════════════════════════════════════╣ +║ ║ +║ 🎯 MARKET RESEARCH COMMANDS: ║ +║ • Research [brand/company] - Comprehensive market analysis ║ +║ • Analyze [brand] vs [competitor] - Competitive comparison ║ +║ • Monitor [brand] news - Real-time news and sentiment tracking ║ +║ • Trends [industry/topic] - Industry trend analysis ║ +║ ║ +║ 📊 EXAMPLE QUERIES: ║ +║ • "Research Tesla's market position in electric vehicles" ║ +║ • "Analyze Netflix vs Disney+ streaming market share" ║ +║ • "Monitor Apple news and competitor responses this week" ║ +║ • "Trends in artificial intelligence and machine learning" ║ +║ • "Compare pricing strategies of Spotify vs Apple Music" ║ +║ • "Research customer sentiment for Samsung Galaxy phones" ║ +║ ║ +║ 🔍 ADVANCED RESEARCH: ║ +║ • "Multi-competitor analysis of ride-sharing market" ║ +║ • "Brand perception analysis for sustainable fashion brands" ║ +║ • "Market entry strategy research for fintech in Europe" ║ +║ • "Social media sentiment tracking for crypto exchanges" ║ +║ ║ +║ 🛠️ SYSTEM COMMANDS: ║ +║ • Help - Show this help message ║ +║ • Exit/Quit - Close the agent ║ +║ ║ +║ 💡 TIPS: ║ +║ - Be specific about your research goals ║ +║ - Mention target markets or regions for localized insights ║ +║ - Include timeframes (e.g., "this month", "Q4 2024") ║ +║ - Ask for actionable recommendations ║ +║ ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +`); + } + + async run(): Promise { + await this.initialize(); + + while (true) { + try { + const userInput = await new Promise((resolve) => { + this.rl.question('💬 Your marketing research request: ', resolve); + }); + + if (userInput.trim()) { + await this.processUserInput(userInput.trim()); + } + } catch (error) { + if (this.config.abortController.signal.aborted) { + break; + } + console.error('❌ Error in main loop:', (error as Error).message); + } + } + } + + shutdown(): void { + console.log('\n👋 Thanks for using Marketing Intelligence Agent!'); + console.log( + `📊 Session stats: ${this.config.iterationCount} interactions, ${this.config.totalTokensUsed} tokens used` + ); + + this.config.abortController.abort(); + this.rl.close(); + process.exit(0); + } +} + +// Create and start the agent +const agent = new MarketingAgent(); + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\n🛑 Received interrupt signal...'); + agent.shutdown(); +}); + +process.on('SIGTERM', () => { + console.log('\n🛑 Received terminate signal...'); + agent.shutdown(); +}); + +// Start the agent +agent.run().catch((error) => { + console.error('💥 Fatal error:', error); + process.exit(1); +}); diff --git a/examples/mcp/agents/marketing/package-lock.json b/examples/mcp/agents/marketing/package-lock.json new file mode 100644 index 0000000..e954cc5 --- /dev/null +++ b/examples/mcp/agents/marketing/package-lock.json @@ -0,0 +1,886 @@ +{ + "name": "marketing-agent", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "marketing-agent", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@inference-gateway/sdk": "^0.7.3", + "axios": "^1.9.0", + "dotenv": "^16.5.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "tsx": "^4.19.4", + "typescript": "^5.3.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inference-gateway/sdk": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@inference-gateway/sdk/-/sdk-0.7.3.tgz", + "integrity": "sha512-UCbapsrblks9A0armTyXIdBM1NHy+8blZNkSP4AeTlHqH8CpVHH2gcgNRyn2SgqewF61kPXkijj0fh1XLZM7OA==", + "license": "MIT", + "engines": { + "node": ">=22.12.0", + "npm": ">=10.9.0" + }, + "peerDependencies": { + "node-fetch": "^2.7.0" + }, + "peerDependenciesMeta": { + "node-fetch": { + "optional": true + } + } + }, + "node_modules/@types/node": { + "version": "20.17.57", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", + "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/examples/mcp/agents/marketing/package.json b/examples/mcp/agents/marketing/package.json new file mode 100644 index 0000000..1180b69 --- /dev/null +++ b/examples/mcp/agents/marketing/package.json @@ -0,0 +1,30 @@ +{ + "name": "marketing-agent", + "version": "1.0.0", + "description": "Marketing Intelligence Agent using Brave Search and MCP tools for comprehensive market research and competitive analysis.", + "main": "index.js", + "private": true, + "scripts": { + "start": "tsx index.ts", + "dev": "tsx watch index.ts" + }, + "keywords": [ + "marketing", + "research", + "brave-search", + "mcp", + "intelligence" + ], + "author": "Inference Gateway Team", + "license": "MIT", + "dependencies": { + "@inference-gateway/sdk": "^0.7.3", + "axios": "^1.9.0", + "dotenv": "^16.5.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "tsx": "^4.19.4", + "typescript": "^5.3.0" + } +} diff --git a/examples/mcp/agents/nextjs/index.ts b/examples/mcp/agents/nextjs/index.ts index abcb538..52a7e07 100644 --- a/examples/mcp/agents/nextjs/index.ts +++ b/examples/mcp/agents/nextjs/index.ts @@ -1,13 +1,6 @@ /** * Interactive NextJS Agent * - * This agent allows maxRetries: 3, - retryDelayMs: 2000, - iterationCount: 0, - totalTokensUsed: 0, - maxTokensPerRequest: 4000, // Reduced from 16000 - maxHistoryLength: 8, // Reduced from 12 to interactively request app development assistance - * using Context7 MCP tools for up-to-date documentation and library information. */ import { @@ -54,16 +47,17 @@ class NextJSAgent { this.config = { client: new InferenceGatewayClient({ baseURL: 'http://inference-gateway:8080/v1', + timeout: 120000, }), provider: (process.env.PROVIDER as Provider) || Provider.groq, model: process.env.LLM || 'llama-3.3-70b-versatile', conversationHistory: [], - maxRetries: 3, - retryDelayMs: 10000, + maxRetries: 5, + retryDelayMs: 5000, iterationCount: 0, totalTokensUsed: 0, - maxTokensPerRequest: 3000, - maxHistoryLength: 10, + maxTokensPerRequest: 2500, + maxHistoryLength: 6, sessionId: process.env.SESSION_ID || randomUUID(), memoryEnabled: true, abortController: new globalThis.AbortController(), @@ -456,7 +450,7 @@ WORKFLOW: 1) Clarify requirements 2) Use Context7 for docs 3) Build in /tmp 4) F console.log('─'.repeat(60)); }, onMCPTool: (toolCall) => { - console.log(`\n🛠️ Context7 Tool: ${toolCall.function.name}`); + console.log(`\n🛠️ NextJS Tool: ${toolCall.function.name}`); try { const args = JSON.parse(toolCall.function.arguments); console.log(`📝 Arguments:`, JSON.stringify(args, null, 2)); diff --git a/examples/mcp/docker-compose-agents.yml b/examples/mcp/docker-compose-agents.yml index 65fa926..ea2a3c0 100644 --- a/examples/mcp/docker-compose-agents.yml +++ b/examples/mcp/docker-compose-agents.yml @@ -32,6 +32,17 @@ services: networks: - inference-network + marketing-agent: + build: + context: ./agents/marketing + dockerfile: Dockerfile + env_file: + - .env + volumes: + - shared-data:/tmp + networks: + - inference-network + volumes: shared-data: driver: local diff --git a/examples/mcp/docker-compose.yml b/examples/mcp/docker-compose.yml index 1a520c0..5da7264 100644 --- a/examples/mcp/docker-compose.yml +++ b/examples/mcp/docker-compose.yml @@ -10,7 +10,7 @@ services: # Enable MCP support MCP_ENABLE: 'true' MCP_EXPOSE: 'true' - MCP_SERVERS: 'http://mcp-filesystem:3000/mcp,http://mcp-web-search:3001/mcp,http://mcp-context7:3002/mcp,http://mcp-npm:3003/mcp,http://mcp-memory:3004/mcp' + MCP_SERVERS: 'http://mcp-filesystem:3000/mcp,http://mcp-context7:3002/mcp,http://mcp-npm:3003/mcp,http://mcp-memory:3004/mcp,http://mcp-brave-search:3005/mcp' # Server settings SERVER_HOST: '0.0.0.0' @@ -38,14 +38,16 @@ services: depends_on: mcp-filesystem: condition: service_healthy - mcp-web-search: - condition: service_healthy + # mcp-web-search: + # condition: service_healthy mcp-context7: condition: service_healthy mcp-npm: condition: service_healthy mcp-memory: condition: service_healthy + mcp-brave-search: + condition: service_healthy networks: - inference-network pull_policy: always @@ -80,33 +82,33 @@ services: start_period: 45s restart: unless-stopped - mcp-web-search: - build: - context: ./mcp-servers/web-search - dockerfile_inline: | - FROM node:22-alpine - WORKDIR /app - RUN apk add --no-cache curl - COPY package.json ./ - RUN npm install - COPY . . - EXPOSE 3001 - CMD ["npm", "start"] - environment: - NODE_ENV: 'production' - MCP_SERVER_NAME: 'web-search' - MCP_SERVER_VERSION: '1.0.0' - PORT: '3001' - HOST: '0.0.0.0' - networks: - - inference-network - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:3001/health'] - interval: 30s - timeout: 10s - retries: 5 - start_period: 45s - restart: unless-stopped + # mcp-web-search: + # build: + # context: ./mcp-servers/web-search + # dockerfile_inline: | + # FROM node:22-alpine + # WORKDIR /app + # RUN apk add --no-cache curl + # COPY package.json ./ + # RUN npm install + # COPY . . + # EXPOSE 3001 + # CMD ["npm", "start"] + # environment: + # NODE_ENV: 'production' + # MCP_SERVER_NAME: 'web-search' + # MCP_SERVER_VERSION: '1.0.0' + # PORT: '3001' + # HOST: '0.0.0.0' + # networks: + # - inference-network + # healthcheck: + # test: ['CMD', 'curl', '-f', 'http://localhost:3001/health'] + # interval: 30s + # timeout: 10s + # retries: 5 + # start_period: 45s + # restart: unless-stopped mcp-context7: build: @@ -198,36 +200,69 @@ services: start_period: 45s restart: unless-stopped - mcp-inspector: + # mcp-inspector: + # build: + # context: . + # dockerfile_inline: | + # FROM node:22-alpine + # WORKDIR /app + # RUN apk add --no-cache curl git + # RUN npm install -g @modelcontextprotocol/inspector + # EXPOSE 6274 + # CMD ["npx", "@modelcontextprotocol/inspector"] + # environment: + # NODE_ENV: 'production' + # CLIENT_PORT: '6274' + # ports: + # - '6274:6274' + # networks: + # - inference-network + # depends_on: + # mcp-filesystem: + # condition: service_healthy + # mcp-web-search: + # condition: service_healthy + # mcp-context7: + # condition: service_healthy + # mcp-npm: + # condition: service_healthy + # mcp-memory: + # condition: service_healthy + # mcp-brave-search: + # condition: service_healthy + # healthcheck: + # test: ['CMD', 'curl', '-f', 'http://localhost:6274'] + # interval: 30s + # timeout: 10s + # retries: 5 + # start_period: 45s + # restart: unless-stopped + + mcp-brave-search: build: - context: . + context: ./mcp-servers/brave-search dockerfile_inline: | FROM node:22-alpine WORKDIR /app - RUN apk add --no-cache curl git - RUN npm install -g @modelcontextprotocol/inspector - EXPOSE 6274 - CMD ["npx", "@modelcontextprotocol/inspector"] + RUN apk add --no-cache curl + COPY package.json ./ + RUN npm install + COPY . . + EXPOSE 3005 + CMD ["npm", "start"] environment: NODE_ENV: 'production' - CLIENT_PORT: '6274' + MCP_SERVER_NAME: 'brave-search' + MCP_SERVER_VERSION: '1.0.0' + MCP_TRANSPORT: 'streamableHttp' + PORT: '3005' + BRAVE_API_KEY: '${BRAVE_API_KEY:-}' ports: - - '6274:6274' + - '3005:3005' networks: - inference-network - depends_on: - mcp-filesystem: - condition: service_healthy - mcp-web-search: - condition: service_healthy - mcp-context7: - condition: service_healthy - mcp-npm: - condition: service_healthy - mcp-memory: - condition: service_healthy healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:6274'] + test: ['CMD', 'curl', '-f', 'http://localhost:3005/health'] interval: 30s timeout: 10s retries: 5 diff --git a/examples/mcp/mcp-servers/brave-search/README.md b/examples/mcp/mcp-servers/brave-search/README.md new file mode 100644 index 0000000..ebc2331 --- /dev/null +++ b/examples/mcp/mcp-servers/brave-search/README.md @@ -0,0 +1,161 @@ +# Brave Search MCP Server + +A Model Context Protocol (MCP) server that provides search capabilities using the Brave Search API with HTTP transport support. + +## Features + +- **Web Search**: Comprehensive web search with SafeSearch filtering +- **News Search**: Recent news articles with content summaries +- **Marketing Research**: Product and brand research with market insights +- **HTTP Transport**: StreamableHTTP transport for web-based integrations +- **Rate Limiting**: Built-in rate limiting with exponential backoff +- **Session Management**: Proper session handling for MCP connections + +## Prerequisites + +- Node.js 18 or higher +- Brave Search API key from [Brave Search API](https://api.search.brave.com/) + +## Installation + +1. Install dependencies: + +```bash +npm install +``` + +2. Set up environment variables: + +```bash +export BRAVE_API_KEY="your-brave-search-api-key" +export PORT=3000 # Optional, defaults to 3000 +export LOG_LEVEL=info # Optional, defaults to info +``` + +## Usage + +### Start the Server + +```bash +npm start +``` + +The server will start on `http://localhost:3000` with the MCP endpoint available at `/mcp`. + +### Health Check + +Check if the server is running: + +```bash +curl http://localhost:3000/health +``` + +### MCP Tools Available + +1. **brave_web_search** + + - Searches the web using Brave Search + - Parameters: `query` (string), `count` (number, optional), `safesearch` (string, optional) + +2. **brave_news_search** + + - Searches for recent news articles + - Parameters: `query` (string), `count` (number, optional), `freshness` (string, optional) + +3. **marketing_research** + - Performs market research and competitive analysis + - Parameters: `query` (string), `focus_area` (string, optional) + +## Configuration + +### Environment Variables + +- `BRAVE_API_KEY`: Your Brave Search API key (required) +- `PORT`: Server port (default: 3000) +- `LOG_LEVEL`: Logging level - debug, info, warn, error (default: info) + +### Rate Limiting + +The server implements rate limiting to respect Brave Search API limits: + +- Maximum 10 requests per second +- Exponential backoff on rate limit errors +- Automatic retry with increasing delays + +## Development + +### Build + +```bash +npm run build +``` + +### Development Mode + +```bash +npm run dev +``` + +### Linting + +```bash +npm run lint +``` + +## API Reference + +### MCP Endpoint + +**POST /mcp** + +- Establishes MCP connection using StreamableHTTP transport +- Handles tool calls and resource requests +- Returns JSON responses with MCP protocol format + +**GET /mcp** + +- Returns server information and available tools +- Used for capability discovery + +**DELETE /mcp** + +- Closes MCP session +- Cleans up resources + +### Example MCP Tool Call + +```json +{ + "jsonrpc": "2.0", + "id": "1", + "method": "tools/call", + "params": { + "name": "brave_web_search", + "arguments": { + "query": "artificial intelligence trends 2024", + "count": 5, + "safesearch": "moderate" + } + } +} +``` + +## Error Handling + +The server includes comprehensive error handling: + +- API rate limiting with retry logic +- Input validation using Zod schemas +- Structured logging for debugging +- Graceful error responses + +## Security + +- Input validation on all parameters +- SafeSearch filtering for web searches +- Rate limiting to prevent abuse +- Structured logging without sensitive data exposure + +## License + +MIT License - see LICENSE file for details. diff --git a/examples/mcp/mcp-servers/brave-search/index.ts b/examples/mcp/mcp-servers/brave-search/index.ts new file mode 100644 index 0000000..8909e6d --- /dev/null +++ b/examples/mcp/mcp-servers/brave-search/index.ts @@ -0,0 +1,736 @@ +/** + * MCP Brave Search Server + * + * This is a Model Context Protocol (MCP) server that provides Brave Search API + * capabilities. It uses the official MCP TypeScript SDK and implements the + * proper MCP protocol with Streamable HTTP transport to bridge stdio-only + * Brave Search containers to HTTP endpoints. + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import axios from 'axios'; +import cors from 'cors'; +import express from 'express'; +import { randomUUID } from 'node:crypto'; +import { setTimeout } from 'node:timers/promises'; +import { z } from 'zod'; +import { createMcpLogger } from './logger.js'; + +const logger = createMcpLogger('mcp-brave-search', '1.0.0'); + +let lastSearchTime = 0; + +// Rate limiting configuration based on plan +const RATE_LIMITS = { + free: { interval: 1000, maxRetries: 5 }, // 1 query/second + base: { interval: 50, maxRetries: 3 }, // 20 queries/second + pro: { interval: 20, maxRetries: 3 }, // 50 queries/second +}; + +const getCurrentRateLimit = () => { + const planType = (process.env.BRAVE_API_PLAN || 'free').toLowerCase(); + return RATE_LIMITS[planType as keyof typeof RATE_LIMITS] || RATE_LIMITS.free; +}; + +/** + * Rate-limited search function to avoid hitting API limits + */ +const rateLimitedBraveSearch = async ( + endpoint: string, + params: Record, + retries?: number +) => { + const rateLimit = getCurrentRateLimit(); + const maxRetries = retries || rateLimit.maxRetries; + + const now = Date.now(); + const timeSinceLastSearch = now - lastSearchTime; + + if (timeSinceLastSearch < rateLimit.interval) { + const delay = rateLimit.interval - timeSinceLastSearch; + logger.info('Rate limiting: delaying search', { + delay, + endpoint, + interval: rateLimit.interval, + plan: process.env.BRAVE_API_PLAN || 'free', + }); + await setTimeout(delay); + } + + lastSearchTime = Date.now(); + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const apiKey = process.env.BRAVE_API_KEY; + if (!apiKey) { + throw new Error('BRAVE_API_KEY environment variable is required'); + } + + const response = await axios.get( + `https://api.search.brave.com/res/v1/${endpoint}`, + { + params, + headers: { + Accept: 'application/json', + 'Accept-Encoding': 'gzip', + 'X-Subscription-Token': apiKey, + }, + timeout: 30000, + } + ); + + logger.info('Brave Search API request successful', { + endpoint, + statusCode: response.status, + attempt, + plan: process.env.BRAVE_API_PLAN || 'free', + }); + + return response.data; + } catch (error: any) { + const is429Error = error.response?.status === 429; + const isRateLimitError = + error.message.includes('rate limit') || + error.message.includes('Too Many Requests'); + + if (is429Error || isRateLimitError) { + if (attempt < maxRetries) { + const baseDelay = rateLimit.interval * Math.pow(2, attempt - 1); + const jitter = Math.random() * 1000; + const backoffDelay = baseDelay + jitter; + + logger.warn('Brave Search rate limit hit, retrying', { + attempt, + retries: maxRetries, + delay: backoffDelay, + endpoint, + error: error.message, + statusCode: error.response?.status, + plan: process.env.BRAVE_API_PLAN || 'free', + }); + + await setTimeout(backoffDelay); + continue; + } else { + logger.error('Brave Search rate limit exceeded after all retries', { + attempts: maxRetries, + endpoint, + error: error.message, + statusCode: error.response?.status, + plan: process.env.BRAVE_API_PLAN || 'free', + }); + } + } + + if (!is429Error && !isRateLimitError) { + logger.error('Brave Search API error (non-rate-limit)', { + endpoint, + error: error.message, + statusCode: error.response?.status, + attempt, + }); + } + + throw error; + } + } +}; + +const app = express(); +app.use(cors()); +app.use(express.json({ limit: '10mb' })); + +app.use((req, res, next) => { + const startTime = Date.now(); + const requestId = randomUUID(); + + (req as any).requestId = requestId; + + logger.info('Incoming request', { + requestId, + method: req.method, + url: req.url, + userAgent: req.get('User-Agent'), + contentType: req.get('Content-Type'), + sessionId: req.headers['mcp-session-id'], + }); + + res.on('finish', () => { + const duration = Date.now() - startTime; + logger.info('Request completed', { + requestId, + method: req.method, + url: req.url, + statusCode: res.statusCode, + duration, + sessionId: req.headers['mcp-session-id'], + }); + }); + + next(); +}); + +const transports = new Map(); + +/** + * Enhanced error handling for tools + */ +function createToolError(error: Error, context: Record = {}) { + logger.error('Tool execution error', { + error: error.message, + stack: error.stack, + context, + }); + + return { + content: [ + { + type: 'text' as const, + text: `Error: ${error.message}`, + }, + ], + isError: true, + }; +} + +/** + * Create and configure the MCP server + */ +function createMcpServer() { + logger.info('Creating MCP server instance'); + + const mcpServer = new McpServer({ + name: 'brave-search', + version: '1.0.0', + }); + + mcpServer.tool( + 'brave_web_search', + 'Search the web using Brave Search API with SafeSearch filtering', + { + query: z.string().min(1).max(500).describe('The search query to execute'), + count: z + .number() + .min(1) + .max(20) + .default(10) + .describe('Maximum number of results to return'), + country: z + .string() + .length(2) + .optional() + .describe('Country code for localized results (e.g., US, GB, DE)'), + safesearch: z + .enum(['strict', 'moderate', 'off']) + .default('moderate') + .describe('Safe search setting'), + freshness: z + .enum(['pd', 'pw', 'pm', 'py']) + .optional() + .describe( + 'Freshness filter: pd=past day, pw=past week, pm=past month, py=past year' + ), + text_decorations: z + .boolean() + .default(false) + .describe('Include text decorations in results'), + }, + async ({ + query, + count = 10, + country, + safesearch = 'moderate', + freshness, + text_decorations = false, + }) => { + const operationId = randomUUID(); + + try { + logger.info('Performing Brave web search', { + operationId, + query, + count, + country, + safesearch, + freshness, + }); + + const params: Record = { + q: query, + count: Math.min(count, 20), + safesearch, + text_decorations, + }; + + if (country) params.country = country; + if (freshness) params.freshness = freshness; + + const searchResults = await rateLimitedBraveSearch( + 'web/search', + params + ); + + if ( + !searchResults?.web?.results || + searchResults.web.results.length === 0 + ) { + logger.warn('No search results found', { operationId, query }); + return { + content: [ + { + type: 'text' as const, + text: `No web search results found for "${query}".`, + }, + ], + }; + } + + const results = searchResults.web.results.slice(0, count); + const formattedResults = results + .map((result: any, index: number) => { + return `${index + 1}. ${result.title}\n ${result.url}\n ${result.description || 'No description available'}\n`; + }) + .join('\n'); + + logger.info('Brave web search completed successfully', { + operationId, + query, + resultCount: results.length, + }); + + return { + content: [ + { + type: 'text' as const, + text: `Brave Web Search Results for "${query}":\n\n${formattedResults}`, + }, + ], + }; + } catch (error: any) { + logger.error('Failed to perform Brave web search', { + operationId, + query, + error: error.message, + stack: error.stack, + }); + + return createToolError( + new Error(`Failed to search Brave: ${error.message}`), + { query, operationId } + ); + } + } + ); + + mcpServer.tool( + 'brave_news_search', + 'Search for recent news articles using Brave Search API', + { + query: z + .string() + .min(1) + .max(500) + .describe('The news search query to execute'), + count: z + .number() + .min(1) + .max(20) + .default(10) + .describe('Maximum number of news results to return'), + country: z + .string() + .length(2) + .optional() + .describe('Country code for localized news (e.g., US, GB, DE)'), + freshness: z + .enum(['pd', 'pw', 'pm', 'py']) + .optional() + .describe( + 'Freshness filter: pd=past day, pw=past week, pm=past month, py=past year' + ), + }, + async ({ query, count = 10, country, freshness }) => { + const operationId = randomUUID(); + + try { + logger.info('Performing Brave news search', { + operationId, + query, + count, + country, + freshness, + }); + + const params: Record = { + q: query, + count: Math.min(count, 20), + }; + + if (country) params.country = country; + if (freshness) params.freshness = freshness; + + const searchResults = await rateLimitedBraveSearch( + 'news/search', + params + ); + + if (!searchResults?.results || searchResults.results.length === 0) { + logger.warn('No news results found', { operationId, query }); + return { + content: [ + { + type: 'text' as const, + text: `No news results found for "${query}".`, + }, + ], + }; + } + + const results = searchResults.results.slice(0, count); + const formattedResults = results + .map((result: any, index: number) => { + const publishedDate = result.age ? ` (${result.age})` : ''; + return `${index + 1}. ${result.title}${publishedDate}\n ${result.url}\n ${result.description || 'No description available'}\n`; + }) + .join('\n'); + + logger.info('Brave news search completed successfully', { + operationId, + query, + resultCount: results.length, + }); + + return { + content: [ + { + type: 'text' as const, + text: `Brave News Search Results for "${query}":\n\n${formattedResults}`, + }, + ], + }; + } catch (error: any) { + logger.error('Failed to perform Brave news search', { + operationId, + query, + error: error.message, + stack: error.stack, + }); + + return createToolError( + new Error(`Failed to search Brave News: ${error.message}`), + { query, operationId } + ); + } + } + ); + + mcpServer.tool( + 'marketing_research', + 'Perform automated competitive market research using Brave Search', + { + brand: z + .string() + .min(1) + .max(100) + .describe('The brand or company to research'), + competitors: z + .array(z.string()) + .optional() + .describe('List of competitor brands to include in research'), + topics: z + .array(z.string()) + .default(['reviews', 'pricing', 'features']) + .describe('Research topics to focus on'), + country: z + .string() + .length(2) + .default('US') + .describe('Country code for localized research'), + }, + async ({ + brand, + competitors = [], + topics = ['reviews', 'pricing', 'features'], + country = 'US', + }) => { + const operationId = randomUUID(); + + try { + logger.info('Performing marketing research', { + operationId, + brand, + competitors, + topics, + country, + }); + + const allBrands = [brand, ...competitors]; + const researchResults = []; + + for (const currentBrand of allBrands) { + for (const topic of topics) { + const query = `${currentBrand} ${topic}`; + + try { + const params = { + q: query, + count: 5, + country, + freshness: 'pm', + }; + + const searchResults = await rateLimitedBraveSearch( + 'web/search', + params + ); + + if (searchResults?.web?.results) { + researchResults.push({ + brand: currentBrand, + topic, + results: searchResults.web.results.slice(0, 3), + }); + } + + await setTimeout(500); + } catch (error: any) { + logger.warn('Failed to search for brand topic', { + brand: currentBrand, + topic, + error: error.message, + }); + } + } + } + + if (researchResults.length === 0) { + return { + content: [ + { + type: 'text' as const, + text: `No marketing research data found for "${brand}".`, + }, + ], + }; + } + + let formattedOutput = `Marketing Research Report for "${brand}"\n`; + formattedOutput += `=`.repeat(50) + '\n\n'; + + const brandResults = researchResults.reduce((acc: any, result) => { + if (!acc[result.brand]) acc[result.brand] = {}; + if (!acc[result.brand][result.topic]) + acc[result.brand][result.topic] = []; + acc[result.brand][result.topic].push(...result.results); + return acc; + }, {}); + + for (const [brandName, brandTopics] of Object.entries(brandResults)) { + formattedOutput += `## ${brandName}\n\n`; + + for (const [topicName, topicResults] of Object.entries( + brandTopics as any + )) { + formattedOutput += `### ${topicName.charAt(0).toUpperCase() + topicName.slice(1)}\n`; + + (topicResults as any[]).forEach((result, index) => { + formattedOutput += `${index + 1}. ${result.title}\n`; + formattedOutput += ` ${result.url}\n`; + formattedOutput += ` ${result.description || 'No description available'}\n\n`; + }); + } + formattedOutput += '\n'; + } + + logger.info('Marketing research completed successfully', { + operationId, + brand, + totalResults: researchResults.length, + }); + + return { + content: [ + { + type: 'text' as const, + text: formattedOutput, + }, + ], + }; + } catch (error: any) { + logger.error('Failed to perform marketing research', { + operationId, + brand, + error: error.message, + stack: error.stack, + }); + + return createToolError( + new Error(`Failed to perform marketing research: ${error.message}`), + { brand, operationId } + ); + } + } + ); + + return mcpServer; +} + +app.post('/mcp', async (req, res) => { + try { + logger.info('MCP POST request received', { + headers: req.headers, + bodyKeys: Object.keys(req.body || {}), + method: req.body?.method, + }); + + const accept = req.headers.accept || req.headers.Accept; + if ( + !accept || + !accept.includes('application/json') || + !accept.includes('text/event-stream') + ) { + logger.info('Adding missing Accept headers for MCP compatibility'); + req.headers.accept = 'application/json, text/event-stream'; + } + + const sessionId = req.headers['mcp-session-id'] as string; + let transport: any; + + if (sessionId && transports.has(sessionId)) { + transport = transports.get(sessionId); + logger.info('Using existing session', { sessionId }); + } else { + const newSessionId = randomUUID(); + logger.info('Creating new MCP session', { sessionId: newSessionId }); + + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => newSessionId, + onsessioninitialized: (initSessionId: string) => { + logger.info('MCP session initialized', { sessionId: initSessionId }); + transports.set(initSessionId, transport); + }, + }); + + transport.onclose = () => { + if ((transport as any).sessionId) { + logger.info('MCP session closed', { + sessionId: (transport as any).sessionId, + }); + transports.delete((transport as any).sessionId); + } + }; + + const server = createMcpServer(); + await server.connect(transport); + + transports.set(newSessionId, transport); + + res.setHeader('mcp-session-id', newSessionId); + } + + await transport.handleRequest(req, res, req.body); + } catch (error: any) { + logger.error('Error handling MCP request', { + error: error.message, + stack: error.stack, + method: req.body?.method, + sessionId: req.headers['mcp-session-id'], + }); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } +}); + +app.get('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string; + if (!sessionId || !transports.has(sessionId)) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + const transport = transports.get(sessionId); + await transport!.handleRequest(req, res); +}); + +app.delete('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string; + if (!sessionId || !transports.has(sessionId)) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + const transport = transports.get(sessionId); + await transport!.handleRequest(req, res); +}); + +app.get('/health', (req, res) => { + const healthStatus = { + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + service: 'mcp-brave-search', + version: '1.0.0', + protocol: 'Model Context Protocol', + transport: 'Streamable HTTP', + api_key_configured: !!process.env.BRAVE_API_KEY, + }; + + logger.info('Health check requested', healthStatus); + + res.json(healthStatus); +}); + +const port = parseInt(process.env.PORT || '3002'); +const host = process.env.HOST || '0.0.0.0'; + +app.listen(port, host, () => { + logger.info(`MCP Brave Search server running on http://${host}:${port}`); + logger.info('Protocol: Model Context Protocol (MCP)'); + logger.info('Transport: Streamable HTTP'); + logger.info('Available endpoints:'); + logger.info(' POST /mcp - MCP protocol endpoint'); + logger.info( + ' GET /mcp - SSE notifications (with session-id header)' + ); + logger.info( + ' DELETE /mcp - Session termination (with session-id header)' + ); + logger.info(' GET /health - Health check'); + logger.info('Available tools:'); + logger.info(' - brave_web_search - Web search using Brave Search API'); + logger.info(' - brave_news_search - News search using Brave Search API'); + logger.info( + ' - marketing_research - Automated competitive market research' + ); + + if (!process.env.BRAVE_API_KEY) { + logger.warn( + 'BRAVE_API_KEY environment variable not set - search functionality will be limited' + ); + } +}); + +process.on('SIGTERM', () => { + logger.info('Received SIGTERM, shutting down gracefully'); + Array.from(transports.values()).forEach((transport: any) => { + if (transport.close) transport.close(); + }); + process.exit(0); +}); + +process.on('SIGINT', () => { + logger.info('Received SIGINT, shutting down gracefully'); + Array.from(transports.values()).forEach((transport: any) => { + if (transport.close) transport.close(); + }); + process.exit(0); +}); diff --git a/examples/mcp/mcp-servers/brave-search/logger.ts b/examples/mcp/mcp-servers/brave-search/logger.ts new file mode 100644 index 0000000..a4788a8 --- /dev/null +++ b/examples/mcp/mcp-servers/brave-search/logger.ts @@ -0,0 +1,114 @@ +/** + * Standardized Winston Logger for MCP Servers + * + * Provides consistent, single-line logging across all MCP servers + * with structured metadata and unified formatting. + */ + +import winston from 'winston'; + +/** + * Create a standardized logger for MCP servers + * @param serviceName - The name of the MCP service + * @param version - The version of the service + * @param logLevel - Override log level (defaults to LOG_LEVEL env var or 'info') + * @returns Configured Winston logger + */ +export function createMcpLogger( + serviceName: string, + version = '1.0.0', + logLevel?: string +): winston.Logger { + const level = logLevel || process.env.LOG_LEVEL || 'info'; + + return winston.createLogger({ + level, + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }), + winston.format.errors({ stack: true }), + winston.format.json() + ), + defaultMeta: { + service: serviceName, + version, + protocol: 'Model Context Protocol', + transport: 'Streamable HTTP', + }, + transports: [ + new winston.transports.Console({ + handleExceptions: true, + handleRejections: true, + }), + ], + }); +} + +/** + * Log MCP request received + */ +export function logMcpRequest( + logger: winston.Logger, + req: any, + additionalMeta: Record = {} +): void { + const sessionId = req.headers['mcp-session-id']; + const method = req.body?.method; + const id = req.body?.id; + + logger.info('MCP request received', { + sessionId, + method, + requestId: id, + userAgent: req.headers['user-agent'], + contentLength: req.headers['content-length'], + ...additionalMeta, + }); +} + +/** + * Log MCP session events + */ +export function logMcpSession( + logger: winston.Logger, + event: string, + sessionId: string, + additionalMeta: Record = {} +): void { + logger.info(`MCP session ${event}`, { + sessionId, + ...additionalMeta, + }); +} + +/** + * Log MCP tool calls + */ +export function logMcpToolCall( + logger: winston.Logger, + toolName: string, + sessionId: string, + args: Record = {}, + additionalMeta: Record = {} +): void { + logger.info(`MCP tool called: ${toolName}`, { + sessionId, + tool: toolName, + args: Object.keys(args), + ...additionalMeta, + }); +} + +/** + * Log MCP errors with context + */ +export function logMcpError( + logger: winston.Logger, + error: Error, + context: Record = {} +): void { + logger.error('MCP error occurred', { + error: error.message, + stack: error.stack, + ...context, + }); +} diff --git a/examples/mcp/mcp-servers/brave-search/package-lock.json b/examples/mcp/mcp-servers/brave-search/package-lock.json new file mode 100644 index 0000000..0bc3ee1 --- /dev/null +++ b/examples/mcp/mcp-servers/brave-search/package-lock.json @@ -0,0 +1,2472 @@ +{ + "name": "mcp-brave-search-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-brave-search-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "axios": "^1.6.0", + "cors": "^2.8.5", + "express": "^4.18.2", + "winston": "^3.17.0", + "zod": "^3.22.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^20.10.0", + "tsx": "^4.19.4", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz", + "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.18.tgz", + "integrity": "sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz", + "integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.57", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", + "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz", + "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tsx": { + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.48", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.48.tgz", + "integrity": "sha512-0X1mz8FtgEIvaxGjdIImYpZEaZMrund9pGXm3M6vM7Reba0e2eI71KPjSCGXBfwKDPwPoywf6waUKc3/tFvX2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/examples/mcp/mcp-servers/brave-search/package.json b/examples/mcp/mcp-servers/brave-search/package.json new file mode 100644 index 0000000..007d619 --- /dev/null +++ b/examples/mcp/mcp-servers/brave-search/package.json @@ -0,0 +1,34 @@ +{ + "name": "mcp-brave-search-server", + "version": "1.0.0", + "description": "MCP Brave Search Server with Streamable HTTP transport bridge", + "type": "module", + "main": "index.ts", + "scripts": { + "start": "tsx index.ts", + "dev": "tsx --watch index.ts", + "build": "tsc", + "lint": "eslint .", + "test": "jest" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "axios": "^1.6.0", + "cors": "^2.8.5", + "express": "^4.18.2", + "winston": "^3.17.0", + "zod": "^3.22.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^20.10.0", + "tsx": "^4.19.4", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "author": "Inference Gateway Team", + "license": "MIT" +} diff --git a/examples/mcp/mcp-servers/brave-search/tsconfig.json b/examples/mcp/mcp-servers/brave-search/tsconfig.json new file mode 100644 index 0000000..34147ee --- /dev/null +++ b/examples/mcp/mcp-servers/brave-search/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./", + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} From 3f13e268b9340d208ca857654ff53670b355b22a Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Mon, 2 Jun 2025 13:23:26 +0000 Subject: [PATCH 37/38] refactor: Improve error handling and logging in MarketingAgent Signed-off-by: Eden Reich --- examples/mcp/agents/marketing/index.ts | 72 +++++++++++++++++++------- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/examples/mcp/agents/marketing/index.ts b/examples/mcp/agents/marketing/index.ts index 729407b..caaea1c 100644 --- a/examples/mcp/agents/marketing/index.ts +++ b/examples/mcp/agents/marketing/index.ts @@ -117,6 +117,26 @@ class MarketingAgent { }); } + /** + * Extract error message from various error types + */ + private getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (typeof error === 'string') { + return error; + } + if (error && typeof error === 'object') { + const errorObj = error as any; + if (errorObj.message) return String(errorObj.message); + if (errorObj.error) return String(errorObj.error); + if (errorObj.reason) return String(errorObj.reason); + return JSON.stringify(error); + } + return String(error); + } + /** * Record an error to both local history and memory */ @@ -136,7 +156,13 @@ class MarketingAgent { toolName: toolCall?.function?.name, toolId: toolCall?.id, toolArguments: toolCall?.function?.arguments - ? JSON.parse(toolCall.function.arguments) + ? (() => { + try { + return JSON.parse(toolCall.function.arguments); + } catch { + return toolCall.function.arguments; + } + })() : undefined, userInput: userInput?.substring(0, 100), recoveryAttempted: false, @@ -144,16 +170,20 @@ class MarketingAgent { this.config.errorHistory.push(errorRecord); - // Keep only last 10 errors to prevent memory bloat if (this.config.errorHistory.length > 10) { this.config.errorHistory = this.config.errorHistory.slice(-10); } console.log(`📝 Recording ${errorType}: ${errorMessage}`); - // Save to memory if enabled if (this.config.memoryEnabled) { - await this.saveErrorToMemory(errorRecord); + try { + await this.saveErrorToMemory(errorRecord); + } catch (memoryError) { + console.log( + `⚠️ Failed to save error to memory: ${this.getErrorMessage(memoryError)}` + ); + } } } @@ -202,13 +232,16 @@ CRITICAL: Call save-error-state tool NOW with this data.`, max_tokens: this.config.maxTokensPerRequest, }, { - onChunk: (chunk) => { - if (chunk.choices?.[0]?.delta?.content) { - process.stdout.write(chunk.choices[0].delta.content); - } + onReasoning: (reasoning) => { + process.stdout.write(reasoning); + }, + onContent: (content) => { + process.stdout.write(content); }, onError: (error) => { - console.error(`❌ Memory save error: ${error}`); + console.error( + `❌ Memory save error: ${this.getErrorMessage(error)}` + ); }, }, this.config.provider, @@ -218,7 +251,7 @@ CRITICAL: Call save-error-state tool NOW with this data.`, console.log('💾 Error state saved to memory system'); } catch (memoryError) { console.log( - `⚠️ Failed to save error to memory: ${(memoryError as Error).message}` + `⚠️ Failed to save error to memory: ${this.getErrorMessage(memoryError)}` ); } } @@ -233,6 +266,8 @@ CRITICAL: Call save-error-state tool NOW with this data.`, return `You are a Marketing Intelligence Agent specializing in market research, competitive analysis, brand monitoring, and marketing strategy development.${errorHistoryPrompt} +Today is ${new Date().toLocaleDateString()}. + CORE CAPABILITIES: - Market research and trend analysis using Brave Search - Competitive intelligence and brand monitoring @@ -273,7 +308,6 @@ Always provide structured, actionable marketing insights with clear next steps.` let attempt = 0; while (attempt < this.config.maxRetries) { try { - // Health check const isHealthy = await this.config.client.healthCheck(); if (!isHealthy) { console.error('❌ Gateway unhealthy. Run: docker-compose up --build'); @@ -282,7 +316,6 @@ Always provide structured, actionable marketing insights with clear next steps.` const tools = await this.config.client.listTools(); - // Check for Brave Search tools const braveSearchTools = tools.data.filter((tool) => [ 'brave_web_search', @@ -355,9 +388,10 @@ Always provide structured, actionable marketing insights with clear next steps.` break; } catch (error) { attempt++; + const errorMessage = this.getErrorMessage(error); console.error( `❌ Initialization attempt ${attempt}/${this.config.maxRetries} failed:`, - (error as Error).message + errorMessage ); if (attempt >= this.config.maxRetries) { @@ -490,10 +524,11 @@ Ready for your marketing research request! 🚀 } }, onError: async (error) => { - console.error(`❌ Stream error: ${error}`); + const errorMessage = this.getErrorMessage(error); + console.error(`❌ Stream error: ${errorMessage}`); await this.recordError( 'stream_error', - (error as Error).message, + errorMessage, 'Streaming chat completion failed', currentToolCall, userInput @@ -522,14 +557,15 @@ Ready for your marketing research request! 🚀 break; } catch (error) { attempt++; + const errorMessage = this.getErrorMessage(error); console.error( `❌ Attempt ${attempt}/${this.config.maxRetries} failed:`, - (error as Error).message + errorMessage ); await this.recordError( 'stream_error', - (error as Error).message, + errorMessage, `Processing attempt ${attempt} failed`, undefined, userInput @@ -604,7 +640,7 @@ Ready for your marketing research request! 🚀 if (this.config.abortController.signal.aborted) { break; } - console.error('❌ Error in main loop:', (error as Error).message); + console.error('❌ Error in main loop:', this.getErrorMessage(error)); } } } From 9f50d027dabfd038b013895a02f72eda3047bae1 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Mon, 2 Jun 2025 17:25:34 +0000 Subject: [PATCH 38/38] docs: Add important note about tool call format in MarketingAgent Signed-off-by: Eden Reich --- examples/mcp/agents/marketing/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/mcp/agents/marketing/index.ts b/examples/mcp/agents/marketing/index.ts index caaea1c..2e1c408 100644 --- a/examples/mcp/agents/marketing/index.ts +++ b/examples/mcp/agents/marketing/index.ts @@ -268,6 +268,8 @@ CRITICAL: Call save-error-state tool NOW with this data.`, Today is ${new Date().toLocaleDateString()}. +IMPORTANT: NEVER use XML for tool calls. Always use JSON format. + CORE CAPABILITIES: - Market research and trend analysis using Brave Search - Competitive intelligence and brand monitoring