This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git


The following commit(s) were added to refs/heads/main by this push:
     new ccede53e6 fix(mcp): Add explicit validation for properties and tools, 
and harden the server (#1036)
ccede53e6 is described below

commit ccede53e68dc9dee775c1a2916949f2afd30522a
Author: Fine0830 <[email protected]>
AuthorDate: Tue Mar 31 14:38:19 2026 +0800

    fix(mcp): Add explicit validation for properties and tools, and harden the 
server (#1036)
---
 CHANGES.md                  |   1 +
 dist/LICENSE                |   2 +-
 docs/operation/mcp/build.md |  79 ++++++--
 docs/operation/mcp/setup.md |  57 +++++-
 mcp/LICENSE                 |   2 +-
 mcp/example-config.json     |   8 +-
 mcp/package-lock.json       |  24 +--
 mcp/src/client/index.ts     |  23 ++-
 mcp/src/config.ts           |  43 +++++
 mcp/src/index.ts            | 454 +++-----------------------------------------
 mcp/src/query/context.ts    |  76 ++++++++
 mcp/src/query/validation.ts | 175 +++++++++++++++++
 mcp/src/server/http.ts      | 196 +++++++++++++++++++
 mcp/src/server/mcp.ts       | 341 +++++++++++++++++++++++++++++++++
 14 files changed, 1004 insertions(+), 477 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index ab476c04a..6728bf657 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -14,6 +14,7 @@ Release Notes.
 ### Bug Fixes
 
 - Fix flaky trace query filtering caused by non-deterministic sidx tag 
ordering and add consistency checks for integration query cases.
+- MCP: Add validation for properties and harden the mcp server.
 
 ## 0.10.0
 
diff --git a/dist/LICENSE b/dist/LICENSE
index 95940cde8..cf0b6aa70 100644
--- a/dist/LICENSE
+++ b/dist/LICENSE
@@ -709,7 +709,7 @@ MIT licenses
     on-finished 2.4.1 MIT
     parseurl 1.3.3 MIT
     path-key 3.1.1 MIT
-    path-to-regexp 8.3.0 MIT
+    path-to-regexp 8.4.1 MIT
     pkce-challenge 5.0.1 MIT
     proxy-addr 2.0.7 MIT
     range-parser 1.2.1 MIT
diff --git a/docs/operation/mcp/build.md b/docs/operation/mcp/build.md
index 57e881cdb..45f15ba10 100644
--- a/docs/operation/mcp/build.md
+++ b/docs/operation/mcp/build.md
@@ -4,7 +4,7 @@ This guide is for developers who want to build the MCP server 
from source and cr
 
 ## Prerequisites
 
-- **Node.js 20+** installed
+- **Node.js 24.6.0+** installed
 - **npm** or **yarn** package manager
 - **TypeScript** knowledge (for development)
 
@@ -13,16 +13,22 @@ This guide is for developers who want to build the MCP 
server from source and cr
 ```
 mcp/
 ├── src/
-│   ├── index.ts              # MCP server implementation
+│   ├── index.ts              # MCP bootstrap entrypoint
+│   ├── config.ts             # Environment parsing and runtime limits
 │   ├── client/
-│   │   ├── index.ts            # BanyanDB HTTP client
-│   │   └── types.ts            # Client type definitions
+│   │   ├── index.ts          # BanyanDB HTTP client
+│   │   └── types.ts          # Client type definitions
 │   ├── query/
-│   │   ├── llm-prompt.ts       # BydbQL prompt generation
-│   │   └── types.ts            # Query type definitions
+│   │   ├── context.ts        # Schema/resource discovery helpers
+│   │   ├── llm-prompt.ts     # BydbQL prompt generation
+│   │   ├── types.ts          # Query type definitions
+│   │   └── validation.ts     # Query and hint validation
+│   ├── server/
+│   │   ├── http.ts           # HTTP transport, auth, and rate limiting
+│   │   └── mcp.ts            # MCP prompt/tool registration and handlers
 │   └── utils/
-│       ├── http.ts             # HTTP utilities
-│       └── logger.ts           # Logging utilities
+│       ├── http.ts           # HTTP utilities
+│       └── logger.ts         # Logging utilities
 ├── tools/
 │   └── checkversion.js        # Version checking utility
 ├── dist/                     # Compiled JavaScript (generated)
@@ -34,11 +40,9 @@ mcp/
 ├── package.json              # Node.js dependencies and scripts
 ├── package-lock.json         # Dependency lock file
 ├── example-config.json       # Example MCP configuration
-├── inspector-config.json     # MCP Inspector configuration
 ├── LICENSE                   # License file
 ├── LICENSE.tpl               # License template
 ├── Makefile                  # Build automation
-└── README.md                 # Project documentation
 ```
 
 ## Building from Source
@@ -58,10 +62,23 @@ npm run build
 
 This compiles TypeScript to JavaScript in the `dist/` directory.
 
+The runtime is organized into small modules:
+
+- `src/index.ts` wires together configuration, transport selection, and 
startup.
+- `src/server/mcp.ts` owns MCP prompt/tool registration and handlers.
+- `src/server/http.ts` owns HTTP transport, request limits, auth, and rate 
limiting.
+- `src/query/validation.ts` centralizes validation for BydbQL and entity hints.
+
 ### 3. Verify Build
 
 ```bash
-node dist/index.js --help
+BANYANDB_ADDRESS=localhost:17900 node dist/index.js
+```
+
+The server starts in `stdio` mode by default. Use `TRANSPORT=http` if you want 
to verify the HTTP transport:
+
+```bash
+TRANSPORT=http BANYANDB_ADDRESS=localhost:17900 node dist/index.js
 ```
 
 ## Development Mode
@@ -78,7 +95,12 @@ This runs `src/index.ts` directly using `tsx`, which is 
faster for iterative dev
 
 ### 1. Make Changes
 
-Edit files in the `src/` directory.
+Edit the relevant files in `src/`:
+
+- transport/bootstrap changes: `src/index.ts`, `src/server/http.ts`
+- tool behavior and prompt wiring: `src/server/mcp.ts`
+- query validation: `src/query/validation.ts`
+- BanyanDB API access: `src/client/index.ts`
 
 ### 2. Format and Lint
 
@@ -109,11 +131,12 @@ node dist/index.js
 Use MCP Inspector to test your changes:
 
 ```bash
-# Build first
+# Terminal 1: start the MCP server
 npm run build
+BANYANDB_ADDRESS=localhost:17900 node dist/index.js
 
-# Run Inspector
-npx @modelcontextprotocol/inspector --config inspector-config.json
+# Terminal 2: run Inspector and point it at the local server command or HTTP 
URL
+npx @modelcontextprotocol/inspector
 ```
 
 ## Debugging
@@ -133,8 +156,7 @@ Create `.vscode/launch.json` in the `mcp` directory:
       "runtimeExecutable": "npx",
       "runtimeArgs": [
         "@modelcontextprotocol/inspector",
-        "--config",
-        "${workspaceFolder}/inspector-config.json"
+        "--cli"
       ],
       "env": {
         "BANYANDB_ADDRESS": "localhost:17900"
@@ -203,7 +225,7 @@ The Dockerfile includes:
 - **Multi-stage build**: Separate build and production stages for smaller 
final image
 - **Security**: Runs as non-root user (`appuser`) for better security
 - **Optimization**: Only production dependencies in final image
-- **Alpine-based**: Uses `node:20-alpine` for minimal image size
+- **Runtime validation**: Supports HTTP safety controls such as `MCP_HOST`, 
`MCP_AUTH_TOKEN`, request size limits, and rate limiting
 
 #### Build Arguments
 
@@ -226,6 +248,18 @@ docker run --rm \
   apache/skywalking-banyandb-mcp:latest
 ```
 
+To test HTTP mode in Docker:
+
+```bash
+docker run --rm \
+  -p 3000:3000 \
+  -e TRANSPORT=http \
+  -e MCP_HOST=0.0.0.0 \
+  -e MCP_AUTH_TOKEN=replace-with-a-strong-random-token \
+  -e BANYANDB_ADDRESS=host.docker.internal:17900 \
+  apache/skywalking-banyandb-mcp:latest
+```
+
 #### Publishing Docker Image
 
 To publish the image to a registry:
@@ -293,7 +327,13 @@ Test with real BanyanDB instance:
 2. Run MCP Inspector:
    ```bash
    npm run build
-   npx @modelcontextprotocol/inspector --config inspector-config.json
+   BANYANDB_ADDRESS=localhost:17900 node dist/index.js
+   ```
+
+   In another terminal:
+
+   ```bash
+   npx @modelcontextprotocol/inspector
    ```
 
 3. Test various queries through the Inspector UI
@@ -358,4 +398,3 @@ Both linting and formatting are integrated into the build 
process via the Makefi
 - [TypeScript Documentation](https://www.typescriptlang.org/docs/)
 - [MCP SDK 
Documentation](https://github.com/modelcontextprotocol/typescript-sdk)
 - [Node.js Documentation](https://nodejs.org/docs/)
-
diff --git a/docs/operation/mcp/setup.md b/docs/operation/mcp/setup.md
index 53e281b0c..542904cc1 100644
--- a/docs/operation/mcp/setup.md
+++ b/docs/operation/mcp/setup.md
@@ -4,7 +4,7 @@ This guide explains how to set up and use the BanyanDB MCP 
server from pre-built
 
 ## Prerequisites
 
-- **Node.js 20+** installed (for binary usage)
+- **Node.js 24.6.0+** installed (for binary usage)
 - **BanyanDB** running and accessible
 - **MCP client** (e.g., Claude Desktop, MCP Inspector, or other MCP-compatible 
clients)
 
@@ -26,6 +26,14 @@ The server starts in stdio mode by default and waits for MCP 
client connections.
 Set the following environment variables:
 
 - `BANYANDB_ADDRESS`: BanyanDB server address (default: `localhost:17900`). 
The server auto-converts gRPC port (17900) to HTTP port (17913).
+- `TRANSPORT`: Transport mode. Default is `stdio`. Set `http` only when you 
want to expose the MCP server over HTTP.
+- `MCP_HOST`: HTTP listen address. Default is `127.0.0.1`.
+- `MCP_PORT`: HTTP listen port. Default is `3000`.
+- `MCP_AUTH_TOKEN`: Optional bearer token for HTTP mode. Required when 
`MCP_HOST` is not a loopback address.
+- `MCP_MAX_BODY_BYTES`: Maximum HTTP request body size. Default is `1048576` 
(1 MiB).
+- `MCP_RATE_LIMIT_WINDOW_MS`: Per-client HTTP rate limit window in 
milliseconds. Default is `60000`.
+- `MCP_RATE_LIMIT_MAX_REQUESTS`: Maximum HTTP requests allowed per client in 
each rate-limit window. Default is `60`.
+- `MCP_RATE_LIMIT_MAX_CLIENTS`: Maximum number of client IDs tracked by the 
in-memory HTTP rate limiter. Default is `10000`.
 
 **Address formats:**
 - `localhost:17900` - Local BanyanDB
@@ -112,7 +120,13 @@ services:
 |----------|----------|---------|-------------|
 | `BANYANDB_ADDRESS` | No | `localhost:17900` | BanyanDB server address. 
Auto-converts gRPC port (17900) to HTTP port (17913). |
 | `TRANSPORT` | No | `stdio` | Transport mode: `stdio` for standard I/O 
(default), `http` for Streamable HTTP. |
+| `MCP_HOST` | No | `127.0.0.1` | HTTP listen address. Only used when 
`TRANSPORT=http`. |
 | `MCP_PORT` | No | `3000` | HTTP port to listen on. Only used when 
`TRANSPORT=http`. |
+| `MCP_AUTH_TOKEN` | Conditionally | unset | Optional bearer token for HTTP 
mode. Required when `MCP_HOST` is not a loopback address. |
+| `MCP_MAX_BODY_BYTES` | No | `1048576` | Maximum HTTP request body size in 
bytes. |
+| `MCP_RATE_LIMIT_WINDOW_MS` | No | `60000` | Per-client HTTP rate-limit 
window in milliseconds. |
+| `MCP_RATE_LIMIT_MAX_REQUESTS` | No | `60` | Maximum HTTP requests allowed 
per client during each rate-limit window. |
+| `MCP_RATE_LIMIT_MAX_CLIENTS` | No | `10000` | Maximum number of client IDs 
retained by the in-memory HTTP rate limiter before oldest entries are evicted. |
 
 ### Verifying BanyanDB Connection
 
@@ -134,21 +148,37 @@ By default the MCP server communicates over standard I/O 
(`TRANSPORT=stdio`), wh
 ### Start in HTTP Mode
 
 ```bash
-# Default port 3000
+# Default loopback bind
 TRANSPORT=http BANYANDB_ADDRESS=localhost:17900 node dist/index.js
 
-# Custom port
-TRANSPORT=http MCP_PORT=8080 BANYANDB_ADDRESS=localhost:17900 node 
dist/index.js
+# Custom host and port
+TRANSPORT=http MCP_HOST=127.0.0.1 MCP_PORT=8080 
BANYANDB_ADDRESS=localhost:17900 node dist/index.js
+
+# Non-loopback host requires an auth token
+TRANSPORT=http \
+MCP_HOST=0.0.0.0 \
+MCP_PORT=3000 \
+MCP_AUTH_TOKEN="$(openssl rand -hex 32)" \
+BANYANDB_ADDRESS=localhost:17900 \
+node dist/index.js
 ```
 
 The server prints the listening address on startup:
 
 ```
-BanyanDB MCP HTTP server listening on :3000/mcp
+BanyanDB MCP HTTP server listening on 127.0.0.1:3000/mcp
 ```
 
 The single endpoint is `POST /mcp`. Other HTTP methods on `/mcp` return `405 
Method Not Allowed`, and all other paths return `404`.
 
+If `MCP_AUTH_TOKEN` is configured, clients must send:
+
+```http
+Authorization: Bearer <token>
+```
+
+HTTP mode also enforces request-size and per-client rate limits using 
`MCP_MAX_BODY_BYTES`, `MCP_RATE_LIMIT_WINDOW_MS`, 
`MCP_RATE_LIMIT_MAX_REQUESTS`, and `MCP_RATE_LIMIT_MAX_CLIENTS`.
+
 ### Configure an MCP Client for HTTP
 
 MCP clients that support the Streamable HTTP transport (e.g. MCP Inspector, 
custom integrations) connect via a URL:
@@ -157,12 +187,17 @@ MCP clients that support the Streamable HTTP transport 
(e.g. MCP Inspector, cust
 {
   "mcpServers": {
     "banyandb": {
-      "url": "http://localhost:3000/mcp";
+      "url": "http://localhost:3000/mcp";,
+      "headers": {
+        "Authorization": "Bearer replace-with-your-token"
+      }
     }
   }
 }
 ```
 
+If you keep the default loopback bind and do not set `MCP_AUTH_TOKEN`, the 
`headers` block is not required.
+
 ### Docker with HTTP Mode
 
 When running in a container, expose the HTTP port and set the transport env 
var:
@@ -172,7 +207,9 @@ docker run -d \
   --name banyandb-mcp \
   -p 3000:3000 \
   -e TRANSPORT=http \
+  -e MCP_HOST=0.0.0.0 \
   -e MCP_PORT=3000 \
+  -e MCP_AUTH_TOKEN=replace-with-a-strong-random-token \
   -e BANYANDB_ADDRESS=banyandb:17900 \
   ghcr.io/apache/skywalking-banyandb-mcp:{COMMIT_ID}
 ```
@@ -194,7 +231,9 @@ services:
     container_name: banyandb-mcp
     environment:
       - TRANSPORT=http
+      - MCP_HOST=0.0.0.0
       - MCP_PORT=3000
+      - MCP_AUTH_TOKEN=replace-with-a-strong-random-token
       - BANYANDB_ADDRESS=banyandb:17900
     ports:
       - "3000:3000"
@@ -219,9 +258,13 @@ services:
 - For Docker, ensure containers are on the same network
 
 **"Command not found: node":**
-- Install Node.js 20+ from [nodejs.org](https://nodejs.org/)
+- Install Node.js 24.6.0+ from [nodejs.org](https://nodejs.org/)
 - Or use Docker image instead
 
+**HTTP mode exits at startup:**
+- If `MCP_HOST` is not `127.0.0.1`, `localhost`, or `::1`, set `MCP_AUTH_TOKEN`
+- Verify `MCP_PORT`, `MCP_MAX_BODY_BYTES`, `MCP_RATE_LIMIT_WINDOW_MS`, 
`MCP_RATE_LIMIT_MAX_REQUESTS`, and `MCP_RATE_LIMIT_MAX_CLIENTS` are positive 
integers
+
 **MCP server not appearing in client:**
 - Verify JSON config is valid
 - Use absolute path to binary
diff --git a/mcp/LICENSE b/mcp/LICENSE
index 032e16ccb..e98f9981b 100644
--- a/mcp/LICENSE
+++ b/mcp/LICENSE
@@ -113,7 +113,7 @@ MIT licenses
     on-finished 2.4.1 MIT
     parseurl 1.3.3 MIT
     path-key 3.1.1 MIT
-    path-to-regexp 8.3.0 MIT
+    path-to-regexp 8.4.1 MIT
     pkce-challenge 5.0.1 MIT
     proxy-addr 2.0.7 MIT
     range-parser 1.2.1 MIT
diff --git a/mcp/example-config.json b/mcp/example-config.json
index d8325a087..7092663f2 100644
--- a/mcp/example-config.json
+++ b/mcp/example-config.json
@@ -4,7 +4,13 @@
       "command": "node",
       "args": ["/absolute/path/to/skywalking-banyandb/mcp/dist/index.js"],
       "env": {
-        "BANYANDB_ADDRESS": "localhost:17900"
+        "BANYANDB_ADDRESS": "localhost:17900",
+        "TRANSPORT": "stdio",
+        "MCP_HOST": "127.0.0.1",
+        "MCP_MAX_BODY_BYTES": "1048576",
+        "MCP_RATE_LIMIT_WINDOW_MS": "60000",
+        "MCP_RATE_LIMIT_MAX_REQUESTS": "60",
+        "MCP_RATE_LIMIT_MAX_CLIENTS": "10000"
       }
     }
   }
diff --git a/mcp/package-lock.json b/mcp/package-lock.json
index 31eec86e2..348ffbc28 100644
--- a/mcp/package-lock.json
+++ b/mcp/package-lock.json
@@ -523,9 +523,9 @@
       "license": "MIT"
     },
     "node_modules/@eslint/config-array/node_modules/brace-expansion": {
-      "version": "1.1.12",
-      "resolved": 
"https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz";,
-      "integrity": 
"sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "version": "1.1.13",
+      "resolved": 
"https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz";,
+      "integrity": 
"sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -621,9 +621,9 @@
       "license": "MIT"
     },
     "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
-      "version": "1.1.12",
-      "resolved": 
"https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz";,
-      "integrity": 
"sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "version": "1.1.13",
+      "resolved": 
"https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz";,
+      "integrity": 
"sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1641,9 +1641,9 @@
       "license": "MIT"
     },
     "node_modules/eslint/node_modules/brace-expansion": {
-      "version": "1.1.12",
-      "resolved": 
"https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz";,
-      "integrity": 
"sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "version": "1.1.13",
+      "resolved": 
"https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz";,
+      "integrity": 
"sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -2636,9 +2636,9 @@
       }
     },
     "node_modules/path-to-regexp": {
-      "version": "8.3.0",
-      "resolved": 
"https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz";,
-      "integrity": 
"sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+      "version": "8.4.1",
+      "resolved": 
"https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz";,
+      "integrity": 
"sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw==",
       "license": "MIT",
       "funding": {
         "type": "opencollective",
diff --git a/mcp/src/client/index.ts b/mcp/src/client/index.ts
index 5617efb69..d94ff1a71 100644
--- a/mcp/src/client/index.ts
+++ b/mcp/src/client/index.ts
@@ -20,6 +20,17 @@
 import type { QueryRequest, QueryResponse, Group, ResourceMetadata } from 
'./types.js';
 import { httpFetch } from '../utils/http.js';
 
+const maxErrorBodyLength = 2048;
+const maxToolResultLength = 32 * 1024;
+
+function truncateText(text: string, maxLength: number): string {
+  if (text.length <= maxLength) {
+    return text;
+  }
+
+  return `${text.slice(0, maxLength)}\n... [truncated ${text.length - 
maxLength} characters]`;
+}
+
 /**
  * BanyanDBClient wraps the BanyanDB HTTP client for executing queries.
  * Uses the HTTP API (grpc-gateway) instead of gRPC to avoid proto dependency 
issues.
@@ -79,7 +90,7 @@ export class BanyanDBClient {
         const errorResponse = (data as { errors: Response }).errors;
         const errorText = await errorResponse.text().catch(() => 
errorResponse.statusText);
         throw new Error(
-          `Query execution failed: ${errorResponse.status} 
${errorResponse.statusText}\n\n${queryDebugInfo}\n\nResponse: ${errorText}`,
+          `Query execution failed: ${errorResponse.status} 
${errorResponse.statusText}\n\n${queryDebugInfo}\n\nResponse: 
${truncateText(errorText, maxErrorBodyLength)}`,
         );
       }
 
@@ -168,7 +179,7 @@ export class BanyanDBClient {
       if (streamResult.elements && Array.isArray(streamResult.elements) && 
streamResult.elements.length === 0) {
         return 'Stream Query Result: No data found (empty result set)';
       }
-      return `Stream Query Result:\n${JSON.stringify(streamResult, null, 2)}`;
+      return truncateText(`Stream Query 
Result:\n${JSON.stringify(streamResult, null, 2)}`, maxToolResultLength);
     }
 
     if (measureResult) {
@@ -188,7 +199,7 @@ export class BanyanDBClient {
           '3. Verify data was written to BanyanDB for this measure'
         );
       }
-      return `Measure Query Result:\n${JSON.stringify(dataPoints, null, 2)}`;
+      return truncateText(`Measure Query Result:\n${JSON.stringify(dataPoints, 
null, 2)}`, maxToolResultLength);
     }
 
     if (traceResult) {
@@ -196,7 +207,7 @@ export class BanyanDBClient {
       if (traceResult.elements && Array.isArray(traceResult.elements) && 
traceResult.elements.length === 0) {
         return 'Trace Query Result: No data found (empty result set)';
       }
-      return `Trace Query Result:\n${JSON.stringify(traceResult, null, 2)}`;
+      return truncateText(`Trace Query Result:\n${JSON.stringify(traceResult, 
null, 2)}`, maxToolResultLength);
     }
 
     if (propertyResult) {
@@ -204,7 +215,7 @@ export class BanyanDBClient {
       if (propertyResult.items && Array.isArray(propertyResult.items) && 
propertyResult.items.length === 0) {
         return 'Property Query Result: No data found (empty result set)';
       }
-      return `Property Query Result:\n${JSON.stringify(propertyResult, null, 
2)}`;
+      return truncateText(`Property Query 
Result:\n${JSON.stringify(propertyResult, null, 2)}`, maxToolResultLength);
     }
 
     if (topnResult) {
@@ -212,7 +223,7 @@ export class BanyanDBClient {
       if (topnResult.lists && Array.isArray(topnResult.lists) && 
topnResult.lists.length === 0) {
         return 'TopN Query Result: No data found (empty result set)';
       }
-      return `TopN Query Result:\n${JSON.stringify(topnResult, null, 2)}`;
+      return truncateText(`TopN Query Result:\n${JSON.stringify(topnResult, 
null, 2)}`, maxToolResultLength);
     }
 
     return 'Unknown result type';
diff --git a/mcp/src/config.ts b/mcp/src/config.ts
new file mode 100644
index 000000000..9e345a445
--- /dev/null
+++ b/mcp/src/config.ts
@@ -0,0 +1,43 @@
+/**
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with this work for
+ * additional information regarding copyright ownership. Apache Software
+ * Foundation (ASF) licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const BANYANDB_ADDRESS = process.env.BANYANDB_ADDRESS || 
'localhost:17900';
+export const TRANSPORT = process.env.TRANSPORT || 'stdio';
+export const MCP_HOST = process.env.MCP_HOST || '127.0.0.1';
+export const MCP_AUTH_TOKEN = process.env.MCP_AUTH_TOKEN?.trim() || '';
+
+function parsePositiveInteger(rawValue: string | undefined, defaultValue: 
string, fieldName: string): number {
+  const parsedValue = parseInt(rawValue || defaultValue, 10);
+  if (!Number.isFinite(parsedValue) || parsedValue <= 0) {
+    throw new Error(`Invalid ${fieldName} value "${rawValue}": must be a 
positive integer.`);
+  }
+
+  return parsedValue;
+}
+
+const mcpPortRaw = parseInt(process.env.MCP_PORT || '3000', 10);
+if (!Number.isFinite(mcpPortRaw) || mcpPortRaw <= 0 || mcpPortRaw > 65535) {
+  throw new Error(`Invalid MCP_PORT value "${process.env.MCP_PORT}": must be 
an integer between 1 and 65535.`);
+}
+
+export const MCP_PORT = mcpPortRaw;
+export const MCP_MAX_BODY_BYTES = 
parsePositiveInteger(process.env.MCP_MAX_BODY_BYTES, `${1024 * 1024}`, 
'MCP_MAX_BODY_BYTES');
+export const MAX_ERROR_MESSAGE_LENGTH = 1024;
+export const MAX_TOOL_RESPONSE_LENGTH = 64 * 1024;
+export const MCP_RATE_LIMIT_WINDOW_MS = 
parsePositiveInteger(process.env.MCP_RATE_LIMIT_WINDOW_MS, '60000', 
'MCP_RATE_LIMIT_WINDOW_MS');
+export const MCP_RATE_LIMIT_MAX_REQUESTS = 
parsePositiveInteger(process.env.MCP_RATE_LIMIT_MAX_REQUESTS, '60', 
'MCP_RATE_LIMIT_MAX_REQUESTS');
+export const MCP_RATE_LIMIT_MAX_CLIENTS = 
parsePositiveInteger(process.env.MCP_RATE_LIMIT_MAX_CLIENTS, '10000', 
'MCP_RATE_LIMIT_MAX_CLIENTS');
diff --git a/mcp/src/index.ts b/mcp/src/index.ts
index dae903ed4..7eec4341d 100644
--- a/mcp/src/index.ts
+++ b/mcp/src/index.ts
@@ -1,456 +1,52 @@
 /**
  * Licensed to Apache Software Foundation (ASF) under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Apache Software Foundation (ASF) licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * license agreements. See the NOTICE file distributed with this work for
+ * additional information regarding copyright ownership. Apache Software
+ * Foundation (ASF) licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
  *
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  */
 
-import { createServer } from 'node:http';
-
 import dotenv from 'dotenv';
-import { z } from 'zod';
 
-import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
 import { StdioServerTransport } from 
'@modelcontextprotocol/sdk/server/stdio.js';
-import { StreamableHTTPServerTransport } from 
'@modelcontextprotocol/sdk/server/streamableHttp.js';
-import { CallToolRequestSchema, ListToolsRequestSchema } from 
'@modelcontextprotocol/sdk/types.js';
-import { BanyanDBClient, ResourceMetadata } from './client/index.js';
-import { generateBydbQL } from './query/llm-prompt.js';
-import { ResourcesByGroup } from './query/types.js';
+import { BanyanDBClient } from './client/index.js';
+import { BANYANDB_ADDRESS, MCP_AUTH_TOKEN, MCP_HOST, TRANSPORT } from 
'./config.js';
+import { isLoopbackHost, startHttpServer } from './server/http.js';
+import { createMcpServer } from './server/mcp.js';
 import { log, setupGlobalErrorHandlers } from './utils/logger.js';
 
-// Load environment variables first
 dotenv.config();
-
-// Set up global error handlers early to catch all errors
 setupGlobalErrorHandlers();
 
-const BANYANDB_ADDRESS = process.env.BANYANDB_ADDRESS || 'localhost:17900';
-const TRANSPORT = process.env.TRANSPORT || 'stdio';
-const mcpPortRaw = parseInt(process.env.MCP_PORT || '3000', 10);
-if (!Number.isFinite(mcpPortRaw) || mcpPortRaw <= 0 || mcpPortRaw > 65535) {
-  throw new Error(`Invalid MCP_PORT value "${process.env.MCP_PORT}": must be 
an integer between 1 and 65535.`);
-}
-const MCP_PORT = mcpPortRaw;
-
-// Prompt schema for generate_BydbQL — used with registerPrompt which requires 
a type cast
-// due to MCP SDK 1.x Zod v3/v4 compatibility layer causing TS2589 deep type 
instantiation.
-const generateBydbQLPromptSchema = {
-  description: z.string().describe(
-    "Natural language description of the query (e.g., 'list the last 30 
minutes service_cpm_minute', 'show the last 30 zipkin spans order by time')",
-  ),
-  resource_type: z.enum(['stream', 'measure', 'trace', 
'property']).optional().describe('Optional resource type hint: stream, measure, 
trace, or property'),
-  resource_name: z.string().optional().describe('Optional resource name hint 
(stream/measure/trace/property name)'),
-  group: z.string().optional().describe('Optional group hint, for example the 
properties group'),
-} as const;
-
-type QueryHints = {
-  description?: string;
-  BydbQL?: string;
-  resource_type?: string;
-  resource_name?: string;
-  group?: string;
-};
-
-function normalizeQueryHints(args: unknown): QueryHints {
-  if (!args || typeof args !== 'object') {
-    return {};
-  }
-
-  const rawArgs = args as Record<string, unknown>;
-  return {
-    description: typeof rawArgs.description === 'string' ? 
rawArgs.description.trim() : undefined,
-    BydbQL: typeof rawArgs.BydbQL === 'string' ? rawArgs.BydbQL.trim() : 
undefined,
-    resource_type: typeof rawArgs.resource_type === 'string' ? 
rawArgs.resource_type.trim() : undefined,
-    resource_name: typeof rawArgs.resource_name === 'string' ? 
rawArgs.resource_name.trim() : undefined,
-    group: typeof rawArgs.group === 'string' ? rawArgs.group.trim() : 
undefined,
-  };
-}
-
-async function loadQueryContext(banyandbClient: BanyanDBClient): Promise<{ 
groups: string[]; resourcesByGroup: ResourcesByGroup }> {
-  let groups: string[] = [];
-  try {
-    const groupsList = await banyandbClient.listGroups();
-    groups = groupsList.map((groupResource) => groupResource.metadata?.name || 
'').filter((groupName) => groupName !== '');
-  } catch (error) {
-    log.warn('Failed to fetch groups, continuing without group information:', 
error instanceof Error ? error.message : String(error));
-  }
-
-  const resourcesByGroup: ResourcesByGroup = {};
-  for (const group of groups) {
-    try {
-      const [streams, measures, traces, properties, topNItems, indexRule] = 
await Promise.all([
-        banyandbClient.listStreams(group).catch(() => []),
-        banyandbClient.listMeasures(group).catch(() => []),
-        banyandbClient.listTraces(group).catch(() => []),
-        banyandbClient.listProperties(group).catch(() => []),
-        banyandbClient.listTopN(group).catch(() => []),
-        banyandbClient.listIndexRule(group).catch(() => []),
-      ]);
-
-      resourcesByGroup[group] = {
-        streams: streams.map((resource) => resource.metadata?.name || 
'').filter((resourceName) => resourceName !== ''),
-        measures: measures.map((resource) => resource.metadata?.name || 
'').filter((resourceName) => resourceName !== ''),
-        traces: traces.map((resource) => resource.metadata?.name || 
'').filter((resourceName) => resourceName !== ''),
-        properties: properties.map((resource) => resource.metadata?.name || 
'').filter((resourceName) => resourceName !== ''),
-        topNItems: topNItems.map((resource) => resource.metadata?.name || 
'').filter((resourceName) => resourceName !== ''),
-        indexRule: indexRule.filter((resource) => !resource.noSort && 
resource.metadata?.name).map((resource) => resource.metadata?.name || ''),
-      };
-    } catch (error) {
-      log.warn(`Failed to fetch resources for group "${group}", continuing:`, 
error instanceof Error ? error.message : String(error));
-      resourcesByGroup[group] = { streams: [], measures: [], traces: [], 
properties: [], topNItems: [], indexRule: [] };
-    }
-  }
-
-  return { groups, resourcesByGroup };
-}
-
-function createMcpServer(banyandbClient: BanyanDBClient): McpServer {
-  const server = new McpServer(
-    {
-      name: 'banyandb-mcp',
-      version: '1.0.0',
-    },
-    {
-      capabilities: {
-        tools: {},
-      },
-    },
-  );
-
-  const registerPrompt = server.registerPrompt.bind(server) as unknown as (
-    name: string,
-    config: {
-      description: string;
-      argsSchema: unknown;
-    },
-    cb: (args: { description: string; resource_type?: string; resource_name?: 
string; group?: string }) => Promise<{
-      messages: Array<{
-        role: 'user';
-        content: {
-          type: 'text';
-          text: string;
-        };
-      }>;
-    }>,
-  ) => unknown;
-
-  registerPrompt(
-    'generate_BydbQL',
-    {
-      description:
-        'Generate the prompt/context needed to derive correct BydbQL from 
natural language and BanyanDB schema hints. Use list_groups_schemas first to 
discover available resources.',
-      argsSchema: generateBydbQLPromptSchema,
-    },
-    async (args) => {
-      const description = args.description.trim();
-      if (!description) {
-        throw new Error('description is required');
-      }
-
-      const { groups, resourcesByGroup } = await 
loadQueryContext(banyandbClient);
-      const prompt = generateBydbQL(description, args, groups, 
resourcesByGroup);
-
-      return {
-        messages: [
-          {
-            role: 'user',
-            content: {
-              type: 'text',
-              text: prompt,
-            },
-          },
-        ],
-      };
-    },
-  );
-
-  // List available tools
-  server.server.setRequestHandler(ListToolsRequestSchema, async () => {
-    return {
-      tools: [
-        {
-          name: 'list_groups_schemas',
-          description:
-            'List available resources in BanyanDB (groups, streams, measures, 
traces, properties). Use this to discover what resources exist before 
querying.',
-          inputSchema: {
-            type: 'object',
-            properties: {
-              resource_type: {
-                type: 'string',
-                description: 'Type of resource to list: groups, streams, 
measures, traces, or properties',
-                enum: ['groups', 'streams', 'measures', 'traces', 
'properties'],
-              },
-              group: {
-                type: 'string',
-                description: 'Group name (required for streams, measures, 
traces, and properties)',
-              },
-            },
-            required: ['resource_type'],
-          },
-        },
-        {
-          name: 'list_resources_bydbql',
-          description: 'Fetch streams, measures, traces, or properties from 
BanyanDB using a BydbQL query.',
-          inputSchema: {
-            type: 'object',
-            properties: {
-              BydbQL: {
-                type: 'string',
-                description: 'BydbQL query to execute against BanyanDB.',
-              },
-              resource_type: {
-                type: 'string',
-                description: 'Optional resource type hint: stream, measure, 
trace, or property',
-                enum: ['stream', 'measure', 'trace', 'property'],
-              },
-              resource_name: {
-                type: 'string',
-                description: 'Optional resource name hint 
(stream/measure/trace/property name)',
-              },
-              group: {
-                type: 'string',
-                description: 'Optional group hint, for example the properties 
group',
-              },
-            },
-            required: ['BydbQL'],
-          },
-        },
-        {
-          name: 'get_generate_bydbql_prompt',
-          description:
-            'Return the full prompt text used by generate_BydbQL, including 
live BanyanDB schema hints. This lets external projects retrieve the prompt 
text directly.',
-          inputSchema: {
-            type: 'object',
-            properties: {
-              description: {
-                type: 'string',
-                description:
-                  "Natural language description of the query (e.g., 'list the 
last 30 minutes service_cpm_minute', 'show the last 30 zipkin spans order by 
time')",
-              },
-              resource_type: {
-                type: 'string',
-                description: 'Optional resource type hint: stream, measure, 
trace, or property',
-                enum: ['stream', 'measure', 'trace', 'property'],
-              },
-              resource_name: {
-                type: 'string',
-                description: 'Optional resource name hint 
(stream/measure/trace/property name)',
-              },
-              group: {
-                type: 'string',
-                description: 'Optional group hint, for example the properties 
group',
-              },
-            },
-            required: ['description'],
-          },
-        },
-      ],
-    };
-  });
-
-  // Handle tool calls
-  server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
-    const { name, arguments: args } = request.params;
-
-    if (name === 'list_groups_schemas') {
-      const resourceType = (args as Record<string, unknown>)?.resource_type as 
string | undefined;
-      if (!resourceType) {
-        throw new Error('resource_type is required and must be one of: groups, 
streams, measures, traces, properties');
-      }
-
-      let result: string;
-
-      if (resourceType === 'groups') {
-        const groups = await banyandbClient.listGroups();
-        const groupNames = groups.map((g) => g.metadata?.name || 
'').filter((n) => n !== '');
-        result = `Available Groups 
(${groupNames.length}):\n${groupNames.join('\n')}`;
-        if (groupNames.length === 0) {
-          result += '\n\nNo groups found. BanyanDB may be empty or not 
configured.';
-        }
-      } else {
-        const group = (args as Record<string, unknown>)?.group as string | 
undefined;
-        if (!group) {
-          throw new Error(`group is required for listing ${resourceType}`);
-        }
-
-        const resourceFetchers: Record<string, () => 
Promise<ResourceMetadata[]>> = {
-          streams: () => banyandbClient.listStreams(group),
-          measures: () => banyandbClient.listMeasures(group),
-          traces: () => banyandbClient.listTraces(group),
-          properties: () => banyandbClient.listProperties(group),
-        };
-
-        const fetcher = resourceFetchers[resourceType];
-        if (!fetcher) {
-          throw new Error(`Invalid resource_type "${resourceType}". Must be 
one of: groups, streams, measures, traces, properties`);
-        }
-
-        const resources = await fetcher();
-        const resourceNames = resources.map((r) => r.metadata?.name || 
'').filter((n) => n !== '');
-        const resourceLabel = resourceType.charAt(0).toUpperCase() + 
resourceType.slice(1);
-
-        result = `Available ${resourceLabel} in group "${group}" 
(${resourceNames.length}):\n${resourceNames.join('\n')}`;
-        if (resourceNames.length === 0) {
-          result += `\n\nNo ${resourceType} found in group "${group}".`;
-        }
-      }
-
-      return { content: [{ type: 'text', text: result }] };
-    }
-
-    if (name === 'list_resources_bydbql') {
-      const queryHints = normalizeQueryHints(args);
-      if (!queryHints.BydbQL) {
-        throw new Error('BydbQL is required');
-      }
-
-      try {
-        const result = await banyandbClient.query(queryHints.BydbQL);
-        const debugParts: string[] = [];
-
-        if (queryHints.resource_type) debugParts.push(`Resource Type: 
${queryHints.resource_type}`);
-        if (queryHints.resource_name) debugParts.push(`Resource Name: 
${queryHints.resource_name}`);
-        if (queryHints.group) debugParts.push(`Group: ${queryHints.group}`);
-
-        const debugInfo = debugParts.length > 0 ? `\n\n=== Debug Information 
===\n${debugParts.join('\n')}` : '';
-        const text = `=== Query Result ===\n\n${result}\n\n=== BydbQL Query 
===\n${queryHints.BydbQL}${debugInfo}`;
-
-        return { content: [{ type: 'text', text }] };
-      } catch (error) {
-        if (error instanceof Error) {
-          if (error.message.includes('timeout') || 
error.message.includes('Timeout')) {
-            return {
-              content: [
-                {
-                  type: 'text',
-                  text:
-                    `Query timeout: ${error.message}\n\n` +
-                    `Possible causes:\n` +
-                    `- BanyanDB is not running or not accessible\n` +
-                    `- Network connectivity issues\n` +
-                    `- BanyanDB is overloaded or slow\n\n` +
-                    `Try:\n` +
-                    `1. Verify BanyanDB is running: curl 
http://localhost:17913/api/healthz\n` +
-                    `2. Check network connectivity\n` +
-                    `3. Use list_groups_schemas to verify BanyanDB is 
accessible`,
-                },
-              ],
-            };
-          }
-          if (error.message.includes('not found') || 
error.message.includes('does not exist') || error.message.includes('Empty 
response')) {
-            return {
-              content: [
-                {
-                  type: 'text',
-                  text:
-                    `Query failed: ${error.message}\n\n` +
-                    `Tip: Use the list_groups_schemas tool to discover 
available resources:\n` +
-                    `- First list groups: list_groups_schemas with 
resource_type="groups"\n` +
-                    `- Then list streams, measures, traces, or properties for 
a target group\n` +
-                    `- Then query using a natural language description and 
optional resource_type, resource_name, or group hints.`,
-                },
-              ],
-            };
-          }
-          throw error;
-        }
-        throw new Error(`Query execution failed: ${String(error)}`);
-      }
-    }
-
-    if (name === 'get_generate_bydbql_prompt') {
-      const queryHints = normalizeQueryHints(args);
-      if (!queryHints.description) {
-        throw new Error('description is required');
-      }
-
-      const { groups, resourcesByGroup } = await 
loadQueryContext(banyandbClient);
-      const prompt = generateBydbQL(queryHints.description, queryHints, 
groups, resourcesByGroup);
-
-      return { content: [{ type: 'text', text: prompt }] };
-    }
-
-    throw new Error(`Unknown tool: ${name}`);
-  });
-
-  return server;
-}
-
 async function main() {
   const banyandbClient = new BanyanDBClient(BANYANDB_ADDRESS);
 
   log.info('Using built-in BydbQL prompt generation with natural language 
support and BanyanDB schema hints. Use the list_groups_schemas tool to discover 
available resources and guide your queries.');
 
   if (TRANSPORT === 'http') {
-    const httpServer = createServer((req, res) => {
-      const { pathname } = new URL(req.url ?? '', 'http://localhost');
-      if (pathname !== '/mcp') {
-        res.writeHead(404, { 'Content-Type': 'application/json' });
-        res.end(JSON.stringify({ error: 'Not found' }));
-        return;
-      }
-
-      if (req.method !== 'POST') {
-        res.writeHead(405, {
-          'Content-Type': 'application/json',
-          Allow: 'POST',
-        });
-        res.end(JSON.stringify({ error: 'Method not allowed' }));
-        return;
-      }
+    if (!isLoopbackHost(MCP_HOST) && !MCP_AUTH_TOKEN) {
+      throw new Error('MCP_AUTH_TOKEN is required when MCP_HOST is not a 
loopback address.');
+    }
 
-      let body = '';
-      req.on('data', (chunk) => { body += chunk; });
-      req.on('end', async () => {
-        let parsedBody: unknown;
-        if (body) {
-          try {
-            parsedBody = JSON.parse(body);
-          } catch {
-            res.writeHead(400, { 'Content-Type': 'application/json' });
-            res.end(JSON.stringify({ error: 'Invalid JSON in request body' }));
-            return;
-          }
-        }
-        try {
-          const transport = new StreamableHTTPServerTransport({ 
sessionIdGenerator: undefined });
-          const mcpServer = createMcpServer(banyandbClient);
-          await mcpServer.connect(transport);
-          await transport.handleRequest(req, res, parsedBody);
-        } catch (error) {
-          if (!res.headersSent) {
-            res.writeHead(500, { 'Content-Type': 'application/json' });
-            res.end(JSON.stringify({ error: error instanceof Error ? 
error.message : String(error) }));
-          }
-        }
-      });
-    });
+    startHttpServer(banyandbClient);
+    return;
+  }
 
-    httpServer.listen(MCP_PORT, () => {
-      log.info(`BanyanDB MCP HTTP server listening on :${MCP_PORT}/mcp`);
-      log.info(`Connecting to BanyanDB at ${BANYANDB_ADDRESS}`);
-    });
-  } else {
-    const mcpServer = createMcpServer(banyandbClient);
-    const transport = new StdioServerTransport();
-    await mcpServer.connect(transport);
+  const mcpServer = createMcpServer(banyandbClient);
+  const transport = new StdioServerTransport();
+  await mcpServer.connect(transport);
 
-    log.info('BanyanDB MCP server started');
-    log.info(`Connecting to BanyanDB at ${BANYANDB_ADDRESS}`);
-  }
+  log.info('BanyanDB MCP server started');
+  log.info(`Connecting to BanyanDB at ${BANYANDB_ADDRESS}`);
 }
 
 main().catch((error) => {
diff --git a/mcp/src/query/context.ts b/mcp/src/query/context.ts
new file mode 100644
index 000000000..5cfcd961b
--- /dev/null
+++ b/mcp/src/query/context.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with this work for
+ * additional information regarding copyright ownership. Apache Software
+ * Foundation (ASF) licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { BanyanDBClient } from '../client/index.js';
+import { ResourcesByGroup } from './types.js';
+import { log } from '../utils/logger.js';
+
+export async function loadQueryContext(
+  banyandbClient: BanyanDBClient,
+): Promise<{ groups: string[]; resourcesByGroup: ResourcesByGroup }> {
+  let groups: string[] = [];
+  try {
+    const groupsList = await banyandbClient.listGroups();
+    groups = groupsList
+      .map((groupResource) => groupResource.metadata?.name || '')
+      .filter((groupName) => groupName !== '');
+  } catch (error) {
+    log.warn(
+      'Failed to fetch groups, continuing without group information:',
+      error instanceof Error ? error.message : String(error),
+    );
+  }
+
+  const resourcesByGroup: ResourcesByGroup = {};
+  for (const group of groups) {
+    try {
+      const [streams, measures, traces, properties, topNItems, indexRule] = 
await Promise.all([
+        banyandbClient.listStreams(group).catch(() => []),
+        banyandbClient.listMeasures(group).catch(() => []),
+        banyandbClient.listTraces(group).catch(() => []),
+        banyandbClient.listProperties(group).catch(() => []),
+        banyandbClient.listTopN(group).catch(() => []),
+        banyandbClient.listIndexRule(group).catch(() => []),
+      ]);
+
+      resourcesByGroup[group] = {
+        streams: streams.map((resource) => resource.metadata?.name || 
'').filter((resourceName) => resourceName !== ''),
+        measures: measures
+          .map((resource) => resource.metadata?.name || '')
+          .filter((resourceName) => resourceName !== ''),
+        traces: traces.map((resource) => resource.metadata?.name || 
'').filter((resourceName) => resourceName !== ''),
+        properties: properties
+          .map((resource) => resource.metadata?.name || '')
+          .filter((resourceName) => resourceName !== ''),
+        topNItems: topNItems
+          .map((resource) => resource.metadata?.name || '')
+          .filter((resourceName) => resourceName !== ''),
+        indexRule: indexRule
+          .filter((resource) => !resource.noSort && resource.metadata?.name)
+          .map((resource) => resource.metadata?.name || ''),
+      };
+    } catch (error) {
+      log.warn(
+        `Failed to fetch resources for group "${group}", continuing:`,
+        error instanceof Error ? error.message : String(error),
+      );
+      resourcesByGroup[group] = { streams: [], measures: [], traces: [], 
properties: [], topNItems: [], indexRule: [] };
+    }
+  }
+
+  return { groups, resourcesByGroup };
+}
diff --git a/mcp/src/query/validation.ts b/mcp/src/query/validation.ts
new file mode 100644
index 000000000..2d91811c4
--- /dev/null
+++ b/mcp/src/query/validation.ts
@@ -0,0 +1,175 @@
+/**
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with this work for
+ * additional information regarding copyright ownership. Apache Software
+ * Foundation (ASF) licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const maxDescriptionLength = 2048;
+const maxIdentifierLength = 256;
+const maxBydbQLLength = 4096;
+const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
+const allowedResourceTypes = ['groups', 'streams', 'measures', 'traces', 
'properties'] as const;
+const allowedQueryHintResourceTypes = ['stream', 'measure', 'trace', 
'property'] as const;
+const disallowedQueryTokenPatterns = [/[;]/, /--/, /\/\*/, /\*\//];
+const allowedQueryPrefixPatterns = [/^\s*SELECT\b/i, /^\s*SHOW\s+TOP\b/i];
+
+export type QueryHints = {
+  description?: string;
+  BydbQL?: string;
+  resource_type?: string;
+  resource_name?: string;
+  group?: string;
+};
+
+export type ListGroupsArgs = {
+  resourceType: (typeof allowedResourceTypes)[number];
+  group?: string;
+};
+
+function containsControlCharacters(value: string): boolean {
+  for (const char of value) {
+    const charCode = char.charCodeAt(0);
+    if (
+      (charCode >= 0x00 && charCode <= 0x08) ||
+      charCode === 0x0b ||
+      charCode === 0x0c ||
+      (charCode >= 0x0e && charCode <= 0x1f) ||
+      charCode === 0x7f
+    ) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+function validateTextInput(fieldName: string, rawValue: string, maxLength: 
number): string {
+  const value = rawValue.trim();
+  if (!value) {
+    throw new Error(`${fieldName} cannot be empty`);
+  }
+  if (value.length > maxLength) {
+    throw new Error(`${fieldName} exceeds the maximum length of ${maxLength} 
characters`);
+  }
+  if (containsControlCharacters(value)) {
+    throw new Error(`${fieldName} contains unsupported control characters`);
+  }
+
+  return value;
+}
+
+function validateIdentifier(fieldName: string, rawValue: string): string {
+  const value = validateTextInput(fieldName, rawValue, maxIdentifierLength);
+  if (!identifierPattern.test(value)) {
+    throw new Error(`${fieldName} contains unsupported characters`);
+  }
+
+  return value;
+}
+
+function validateResourceType(rawValue: string): (typeof 
allowedResourceTypes)[number] {
+  const value = validateTextInput('resource_type', rawValue, 32).toLowerCase();
+  if (!allowedResourceTypes.includes(value as (typeof 
allowedResourceTypes)[number])) {
+    throw new Error('resource_type must be one of: groups, streams, measures, 
traces, properties');
+  }
+
+  return value as (typeof allowedResourceTypes)[number];
+}
+
+function validateQueryHintResourceType(rawValue: string): (typeof 
allowedQueryHintResourceTypes)[number] {
+  const value = validateTextInput('resource_type', rawValue, 32).toLowerCase();
+  if (!allowedQueryHintResourceTypes.includes(value as (typeof 
allowedQueryHintResourceTypes)[number])) {
+    throw new Error('resource_type must be one of: stream, measure, trace, 
property');
+  }
+
+  return value as (typeof allowedQueryHintResourceTypes)[number];
+}
+
+function validateBydbQL(rawValue: string): string {
+  const value = validateTextInput('BydbQL', rawValue, maxBydbQLLength);
+  if (!allowedQueryPrefixPatterns.some((pattern) => pattern.test(value))) {
+    throw new Error('BydbQL must be a read-only SELECT or SHOW TOP query');
+  }
+  for (const disallowedPattern of disallowedQueryTokenPatterns) {
+    if (disallowedPattern.test(value)) {
+      throw new Error('BydbQL contains unsupported multi-statement or comment 
syntax');
+    }
+  }
+
+  return value;
+}
+
+export function normalizeQueryHints(args: unknown): QueryHints {
+  if (!args || typeof args !== 'object') {
+    return {};
+  }
+
+  const rawArgs = args as Record<string, unknown>;
+  return {
+    description: typeof rawArgs.description === 'string' ? 
rawArgs.description.trim() : undefined,
+    BydbQL: typeof rawArgs.BydbQL === 'string' ? rawArgs.BydbQL.trim() : 
undefined,
+    resource_type: typeof rawArgs.resource_type === 'string' ? 
rawArgs.resource_type.trim() : undefined,
+    resource_name: typeof rawArgs.resource_name === 'string' ? 
rawArgs.resource_name.trim() : undefined,
+    group: typeof rawArgs.group === 'string' ? rawArgs.group.trim() : 
undefined,
+  };
+}
+
+export function validateListGroupsArgs(args: unknown): ListGroupsArgs {
+  if (!args || typeof args !== 'object') {
+    throw new Error('tool arguments must be an object');
+  }
+
+  const rawArgs = args as Record<string, unknown>;
+  const rawResourceType = rawArgs.resource_type;
+  if (typeof rawResourceType !== 'string') {
+    throw new Error('resource_type is required and must be one of: groups, 
streams, measures, traces, properties');
+  }
+
+  const resourceType = validateResourceType(rawResourceType);
+  if (resourceType === 'groups') {
+    return { resourceType };
+  }
+
+  const rawGroup = rawArgs.group;
+  if (typeof rawGroup !== 'string') {
+    throw new Error(`group is required for listing ${resourceType}`);
+  }
+
+  return {
+    resourceType,
+    group: validateIdentifier('group', rawGroup),
+  };
+}
+
+export function validateQueryHints(queryHints: QueryHints): QueryHints {
+  const validatedHints: QueryHints = {};
+
+  if (queryHints.description) {
+    validatedHints.description = validateTextInput('description', 
queryHints.description, maxDescriptionLength);
+  }
+  if (queryHints.BydbQL) {
+    validatedHints.BydbQL = validateBydbQL(queryHints.BydbQL);
+  }
+  if (queryHints.resource_type) {
+    validatedHints.resource_type = 
validateQueryHintResourceType(queryHints.resource_type);
+  }
+  if (queryHints.resource_name) {
+    validatedHints.resource_name = validateIdentifier('resource_name', 
queryHints.resource_name);
+  }
+  if (queryHints.group) {
+    validatedHints.group = validateIdentifier('group', queryHints.group);
+  }
+
+  return validatedHints;
+}
diff --git a/mcp/src/server/http.ts b/mcp/src/server/http.ts
new file mode 100644
index 000000000..cf1c2534c
--- /dev/null
+++ b/mcp/src/server/http.ts
@@ -0,0 +1,196 @@
+/**
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with this work for
+ * additional information regarding copyright ownership. Apache Software
+ * Foundation (ASF) licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createServer, IncomingMessage } from 'node:http';
+
+import { StreamableHTTPServerTransport } from 
'@modelcontextprotocol/sdk/server/streamableHttp.js';
+import { BanyanDBClient } from '../client/index.js';
+import {
+  BANYANDB_ADDRESS,
+  MAX_ERROR_MESSAGE_LENGTH,
+  MCP_AUTH_TOKEN,
+  MCP_HOST,
+  MCP_MAX_BODY_BYTES,
+  MCP_PORT,
+  MCP_RATE_LIMIT_MAX_CLIENTS,
+  MCP_RATE_LIMIT_MAX_REQUESTS,
+  MCP_RATE_LIMIT_WINDOW_MS,
+} from '../config.js';
+import { createMcpServer, truncateText } from './mcp.js';
+import { log } from '../utils/logger.js';
+
+type RateLimitEntry = {
+  count: number;
+  windowStart: number;
+};
+
+const rateLimitByClient = new Map<string, RateLimitEntry>();
+
+function getAuthorizationToken(req: IncomingMessage): string {
+  const authHeader = req.headers.authorization;
+  if (!authHeader) {
+    return '';
+  }
+
+  const bearerPrefix = 'Bearer ';
+  return authHeader.startsWith(bearerPrefix) ? 
authHeader.slice(bearerPrefix.length).trim() : '';
+}
+
+export function isLoopbackHost(host: string): boolean {
+  return host === '127.0.0.1' || host === 'localhost' || host === '::1';
+}
+
+function getClientIdentifier(req: IncomingMessage): string {
+  const forwardedFor = req.headers['x-forwarded-for'];
+  if (typeof forwardedFor === 'string' && forwardedFor.trim()) {
+    return forwardedFor.split(',')[0].trim();
+  }
+
+  return req.socket.remoteAddress || 'unknown';
+}
+
+function pruneExpiredRateLimitEntries(now: number): void {
+  for (const [clientId, currentEntry] of rateLimitByClient.entries()) {
+    if (now - currentEntry.windowStart >= MCP_RATE_LIMIT_WINDOW_MS) {
+      rateLimitByClient.delete(clientId);
+    }
+  }
+}
+
+function evictOldestRateLimitEntry(): void {
+  const oldestEntry = rateLimitByClient.entries().next().value;
+  if (oldestEntry) {
+    const [clientId] = oldestEntry;
+    rateLimitByClient.delete(clientId);
+  }
+}
+
+function isRateLimited(clientId: string, now: number): boolean {
+  pruneExpiredRateLimitEntries(now);
+
+  const currentEntry = rateLimitByClient.get(clientId);
+  if (!currentEntry || now - currentEntry.windowStart >= 
MCP_RATE_LIMIT_WINDOW_MS) {
+    if (!currentEntry && rateLimitByClient.size >= MCP_RATE_LIMIT_MAX_CLIENTS) 
{
+      evictOldestRateLimitEntry();
+    }
+    rateLimitByClient.set(clientId, { count: 1, windowStart: now });
+    return false;
+  }
+
+  if (currentEntry.count >= MCP_RATE_LIMIT_MAX_REQUESTS) {
+    return true;
+  }
+
+  rateLimitByClient.delete(clientId);
+  currentEntry.count += 1;
+  rateLimitByClient.set(clientId, currentEntry);
+  return false;
+}
+
+export function startHttpServer(banyandbClient: BanyanDBClient): void {
+  const httpServer = createServer((req, res) => {
+    const { pathname } = new URL(req.url ?? '', 'http://localhost');
+    if (pathname !== '/mcp') {
+      res.writeHead(404, { 'Content-Type': 'application/json' });
+      res.end(JSON.stringify({ error: 'Not found' }));
+      return;
+    }
+
+    if (req.method !== 'POST') {
+      res.writeHead(405, {
+        'Content-Type': 'application/json',
+        Allow: 'POST',
+      });
+      res.end(JSON.stringify({ error: 'Method not allowed' }));
+      return;
+    }
+
+    if (MCP_AUTH_TOKEN && getAuthorizationToken(req) !== MCP_AUTH_TOKEN) {
+      res.writeHead(401, {
+        'Content-Type': 'application/json',
+        'WWW-Authenticate': 'Bearer',
+      });
+      res.end(JSON.stringify({ error: 'Unauthorized' }));
+      return;
+    }
+
+    const clientId = getClientIdentifier(req);
+    if (isRateLimited(clientId, Date.now())) {
+      res.writeHead(429, {
+        'Content-Type': 'application/json',
+        'Retry-After': `${Math.ceil(MCP_RATE_LIMIT_WINDOW_MS / 1000)}`,
+      });
+      res.end(JSON.stringify({ error: 'Too many requests' }));
+      return;
+    }
+
+    const bodyChunks: Buffer[] = [];
+    let receivedBytes = 0;
+    let requestTooLarge = false;
+    req.on('data', (chunk: Buffer) => {
+      receivedBytes += chunk.length;
+      if (receivedBytes > MCP_MAX_BODY_BYTES) {
+        requestTooLarge = true;
+        res.writeHead(413, { 'Content-Type': 'application/json' });
+        res.end(JSON.stringify({ error: `Request body too large. Limit is 
${MCP_MAX_BODY_BYTES} bytes.` }));
+        req.destroy();
+        return;
+      }
+
+      bodyChunks.push(chunk);
+    });
+    req.on('end', async () => {
+      if (requestTooLarge) {
+        return;
+      }
+
+      let parsedBody: unknown;
+      if (receivedBytes > 0) {
+        try {
+          parsedBody = JSON.parse(Buffer.concat(bodyChunks, 
receivedBytes).toString('utf-8'));
+        } catch {
+          res.writeHead(400, { 'Content-Type': 'application/json' });
+          res.end(JSON.stringify({ error: 'Invalid JSON in request body' }));
+          return;
+        }
+      }
+
+      try {
+        const transport = new StreamableHTTPServerTransport({ 
sessionIdGenerator: undefined });
+        const mcpServer = createMcpServer(banyandbClient);
+        await mcpServer.connect(transport);
+        await transport.handleRequest(req, res, parsedBody);
+      } catch (error) {
+        if (!res.headersSent) {
+          res.writeHead(500, { 'Content-Type': 'application/json' });
+          res.end(
+            JSON.stringify({
+              error: truncateText(error instanceof Error ? error.message : 
String(error), MAX_ERROR_MESSAGE_LENGTH),
+            }),
+          );
+        }
+      }
+    });
+  });
+
+  httpServer.listen(MCP_PORT, MCP_HOST, () => {
+    log.info(`BanyanDB MCP HTTP server listening on 
${MCP_HOST}:${MCP_PORT}/mcp`);
+    log.info(`Connecting to BanyanDB at ${BANYANDB_ADDRESS}`);
+    log.info(`HTTP auth ${MCP_AUTH_TOKEN ? 'enabled' : 'disabled'}`);
+    log.info(`HTTP rate limit ${MCP_RATE_LIMIT_MAX_REQUESTS} requests per 
${MCP_RATE_LIMIT_WINDOW_MS}ms`);
+  });
+}
diff --git a/mcp/src/server/mcp.ts b/mcp/src/server/mcp.ts
new file mode 100644
index 000000000..1f237061b
--- /dev/null
+++ b/mcp/src/server/mcp.ts
@@ -0,0 +1,341 @@
+/**
+ * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * license agreements. See the NOTICE file distributed with this work for
+ * additional information regarding copyright ownership. Apache Software
+ * Foundation (ASF) licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { z } from 'zod';
+
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { CallToolRequestSchema, ListToolsRequestSchema } from 
'@modelcontextprotocol/sdk/types.js';
+import { BanyanDBClient, ResourceMetadata } from '../client/index.js';
+import { MAX_TOOL_RESPONSE_LENGTH } from '../config.js';
+import { loadQueryContext } from '../query/context.js';
+import { generateBydbQL } from '../query/llm-prompt.js';
+import { normalizeQueryHints, validateListGroupsArgs, validateQueryHints } 
from '../query/validation.js';
+
+export function truncateText(text: string, maxLength: number): string {
+  if (text.length <= maxLength) {
+    return text;
+  }
+
+  return `${text.slice(0, maxLength)}\n... [truncated ${text.length - 
maxLength} characters]`;
+}
+
+const generateBydbQLPromptSchema = {
+  description: z
+    .string()
+    .describe(
+      "Natural language description of the query (e.g., 'list the last 30 
minutes service_cpm_minute', 'show the last 30 zipkin spans order by time')",
+    ),
+  resource_type: z
+    .enum(['stream', 'measure', 'trace', 'property'])
+    .optional()
+    .describe('Optional resource type hint: stream, measure, trace, or 
property'),
+  resource_name: z.string().optional().describe('Optional resource name hint 
(stream/measure/trace/property name)'),
+  group: z.string().optional().describe('Optional group hint, for example the 
properties group'),
+} as const;
+
+function buildListToolsResponse() {
+  return {
+    tools: [
+      {
+        name: 'list_groups_schemas',
+        description:
+          'List available resources in BanyanDB (groups, streams, measures, 
traces, properties). Use this to discover what resources exist before 
querying.',
+        inputSchema: {
+          type: 'object',
+          properties: {
+            resource_type: {
+              type: 'string',
+              description: 'Type of resource to list: groups, streams, 
measures, traces, or properties',
+              enum: ['groups', 'streams', 'measures', 'traces', 'properties'],
+            },
+            group: {
+              type: 'string',
+              description: 'Group name (required for streams, measures, 
traces, and properties)',
+            },
+          },
+          required: ['resource_type'],
+        },
+      },
+      {
+        name: 'list_resources_bydbql',
+        description: 'Fetch streams, measures, traces, or properties from 
BanyanDB using a BydbQL query.',
+        inputSchema: {
+          type: 'object',
+          properties: {
+            BydbQL: {
+              type: 'string',
+              description: 'BydbQL query to execute against BanyanDB.',
+            },
+            resource_type: {
+              type: 'string',
+              description: 'Optional resource type hint: stream, measure, 
trace, or property',
+              enum: ['stream', 'measure', 'trace', 'property'],
+            },
+            resource_name: {
+              type: 'string',
+              description: 'Optional resource name hint 
(stream/measure/trace/property name)',
+            },
+            group: {
+              type: 'string',
+              description: 'Optional group hint, for example the properties 
group',
+            },
+          },
+          required: ['BydbQL'],
+        },
+      },
+      {
+        name: 'get_generate_bydbql_prompt',
+        description:
+          'Return the full prompt text used by generate_BydbQL, including live 
BanyanDB schema hints. This lets external projects retrieve the prompt text 
directly.',
+        inputSchema: {
+          type: 'object',
+          properties: {
+            description: {
+              type: 'string',
+              description:
+                "Natural language description of the query (e.g., 'list the 
last 30 minutes service_cpm_minute', 'show the last 30 zipkin spans order by 
time')",
+            },
+            resource_type: {
+              type: 'string',
+              description: 'Optional resource type hint: stream, measure, 
trace, or property',
+              enum: ['stream', 'measure', 'trace', 'property'],
+            },
+            resource_name: {
+              type: 'string',
+              description: 'Optional resource name hint 
(stream/measure/trace/property name)',
+            },
+            group: {
+              type: 'string',
+              description: 'Optional group hint, for example the properties 
group',
+            },
+          },
+          required: ['description'],
+        },
+      },
+    ],
+  };
+}
+
+async function handleListGroupsSchemas(banyandbClient: BanyanDBClient, args: 
unknown) {
+  const { resourceType, group } = validateListGroupsArgs(args);
+
+  let result: string;
+
+  if (resourceType === 'groups') {
+    const groups = await banyandbClient.listGroups();
+    const groupNames = groups
+      .map((groupResource) => groupResource.metadata?.name || '')
+      .filter((groupName) => groupName !== '');
+    result = `Available Groups 
(${groupNames.length}):\n${groupNames.join('\n')}`;
+    if (groupNames.length === 0) {
+      result += '\n\nNo groups found. BanyanDB may be empty or not 
configured.';
+    }
+  } else {
+    if (!group) {
+      throw new Error(`group is required for listing ${resourceType}`);
+    }
+
+    const resourceFetchers: Record<string, () => Promise<ResourceMetadata[]>> 
= {
+      streams: () => banyandbClient.listStreams(group),
+      measures: () => banyandbClient.listMeasures(group),
+      traces: () => banyandbClient.listTraces(group),
+      properties: () => banyandbClient.listProperties(group),
+    };
+
+    const fetcher = resourceFetchers[resourceType];
+    if (!fetcher) {
+      throw new Error(
+        `Invalid resource_type "${resourceType}". Must be one of: groups, 
streams, measures, traces, properties`,
+      );
+    }
+
+    const resources = await fetcher();
+    const resourceNames = resources
+      .map((resource) => resource.metadata?.name || '')
+      .filter((resourceName) => resourceName !== '');
+    const resourceLabel = resourceType.charAt(0).toUpperCase() + 
resourceType.slice(1);
+
+    result = `Available ${resourceLabel} in group "${group}" 
(${resourceNames.length}):\n${resourceNames.join('\n')}`;
+    if (resourceNames.length === 0) {
+      result += `\n\nNo ${resourceType} found in group "${group}".`;
+    }
+  }
+
+  return { content: [{ type: 'text', text: result }] };
+}
+
+async function handleListResourcesBydbql(banyandbClient: BanyanDBClient, args: 
unknown) {
+  const queryHints = validateQueryHints(normalizeQueryHints(args));
+  if (!queryHints.BydbQL) {
+    throw new Error('BydbQL is required');
+  }
+
+  try {
+    const result = await banyandbClient.query(queryHints.BydbQL);
+    const debugParts: string[] = [];
+
+    if (queryHints.resource_type) debugParts.push(`Resource Type: 
${queryHints.resource_type}`);
+    if (queryHints.resource_name) debugParts.push(`Resource Name: 
${queryHints.resource_name}`);
+    if (queryHints.group) debugParts.push(`Group: ${queryHints.group}`);
+
+    const debugInfo = debugParts.length > 0 ? `\n\n=== Debug Information 
===\n${debugParts.join('\n')}` : '';
+    const text = truncateText(
+      `=== Query Result ===\n\n${result}\n\n=== BydbQL Query 
===\n${queryHints.BydbQL}${debugInfo}`,
+      MAX_TOOL_RESPONSE_LENGTH,
+    );
+
+    return { content: [{ type: 'text', text }] };
+  } catch (error) {
+    if (error instanceof Error) {
+      if (error.message.includes('timeout') || 
error.message.includes('Timeout')) {
+        return {
+          content: [
+            {
+              type: 'text',
+              text:
+                `Query timeout: ${error.message}\n\n` +
+                `Possible causes:\n` +
+                `- BanyanDB is not running or not accessible\n` +
+                `- Network connectivity issues\n` +
+                `- BanyanDB is overloaded or slow\n\n` +
+                `Try:\n` +
+                `1. Verify BanyanDB is running: curl 
http://localhost:17913/api/healthz\n` +
+                `2. Check network connectivity\n` +
+                `3. Use list_groups_schemas to verify BanyanDB is accessible`,
+            },
+          ],
+        };
+      }
+      if (
+        error.message.includes('not found') ||
+        error.message.includes('does not exist') ||
+        error.message.includes('Empty response')
+      ) {
+        return {
+          content: [
+            {
+              type: 'text',
+              text:
+                `Query failed: ${error.message}\n\n` +
+                `Tip: Use the list_groups_schemas tool to discover available 
resources:\n` +
+                `- First list groups: list_groups_schemas with 
resource_type="groups"\n` +
+                `- Then list streams, measures, traces, or properties for a 
target group\n` +
+                `- Then query using a natural language description and 
optional resource_type, resource_name, or group hints.`,
+            },
+          ],
+        };
+      }
+      throw error;
+    }
+    throw new Error(`Query execution failed: ${String(error)}`);
+  }
+}
+
+async function handleGetGenerateBydbqlPrompt(banyandbClient: BanyanDBClient, 
args: unknown) {
+  const queryHints = validateQueryHints(normalizeQueryHints(args));
+  if (!queryHints.description) {
+    throw new Error('description is required');
+  }
+
+  const { groups, resourcesByGroup } = await loadQueryContext(banyandbClient);
+  const prompt = generateBydbQL(queryHints.description, queryHints, groups, 
resourcesByGroup);
+
+  return { content: [{ type: 'text', text: prompt }] };
+}
+
+export function createMcpServer(banyandbClient: BanyanDBClient): McpServer {
+  const server = new McpServer(
+    {
+      name: 'banyandb-mcp',
+      version: '1.0.0',
+    },
+    {
+      capabilities: {
+        tools: {},
+      },
+    },
+  );
+
+  const registerPrompt = server.registerPrompt.bind(server) as unknown as (
+    name: string,
+    config: { description: string; argsSchema: unknown },
+    cb: (args: { description: string; resource_type?: string; resource_name?: 
string; group?: string }) => Promise<{
+      messages: Array<{
+        role: 'user';
+        content: {
+          type: 'text';
+          text: string;
+        };
+      }>;
+    }>,
+  ) => unknown;
+
+  registerPrompt(
+    'generate_BydbQL',
+    {
+      description:
+        'Generate the prompt/context needed to derive correct BydbQL from 
natural language and BanyanDB schema hints. Use list_groups_schemas first to 
discover available resources.',
+      argsSchema: generateBydbQLPromptSchema,
+    },
+    async (args) => {
+      const validatedHints = validateQueryHints({
+        description: args.description,
+        resource_type: args.resource_type,
+        resource_name: args.resource_name,
+        group: args.group,
+      });
+      const description = validatedHints.description;
+      if (!description) {
+        throw new Error('description is required');
+      }
+
+      const { groups, resourcesByGroup } = await 
loadQueryContext(banyandbClient);
+      const prompt = generateBydbQL(description, validatedHints, groups, 
resourcesByGroup);
+
+      return {
+        messages: [
+          {
+            role: 'user',
+            content: {
+              type: 'text',
+              text: prompt,
+            },
+          },
+        ],
+      };
+    },
+  );
+
+  server.server.setRequestHandler(ListToolsRequestSchema, async () => 
buildListToolsResponse());
+  server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
+    const { name, arguments: args } = request.params;
+
+    if (name === 'list_groups_schemas') {
+      return handleListGroupsSchemas(banyandbClient, args);
+    }
+    if (name === 'list_resources_bydbql') {
+      return handleListResourcesBydbql(banyandbClient, args);
+    }
+    if (name === 'get_generate_bydbql_prompt') {
+      return handleGetGenerateBydbqlPrompt(banyandbClient, args);
+    }
+
+    throw new Error(`Unknown tool: ${name}`);
+  });
+
+  return server;
+}

Reply via email to