From c16b8bea31948df5ad52a96f1d2a367808ecac88 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Tue, 27 May 2025 18:31:19 +0000 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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; +};