chore: Update environment configuration and package dependencies for MCP server

- Change MCP_SERVER in .env.example to use port 7123
- Add USE_STDIO_TRANSPORT flag in .env.example for stdio transport mode
- Update bun.lock to include new dependencies: cors, express, ajv, and their type definitions
- Add new scripts for building and running the MCP server with stdio transport
- Introduce PUBLISHING.md for npm publishing guidelines
- Enhance README with detailed setup instructions and tool descriptions
This commit is contained in:
jango-blockchained
2025-03-17 17:55:38 +01:00
parent 575e16f2fa
commit 1bc11de465
37 changed files with 5947 additions and 252 deletions

View File

@@ -3,7 +3,8 @@ NODE_ENV=development
PORT=7123
DEBUG=false
LOG_LEVEL=info
MCP_SERVER=http://localhost:3000
MCP_SERVER=http://localhost:7123
USE_STDIO_TRANSPORT=true
# Home Assistant Configuration
HASS_HOST=http://homeassistant.local:8123

96
PUBLISHING.md Normal file
View File

@@ -0,0 +1,96 @@
# Publishing to npm
This document outlines the steps to publish the Home Assistant MCP server to npm.
## Prerequisites
1. You need an npm account. Create one at [npmjs.com](https://www.npmjs.com/signup) if you don't have one.
2. You need to be logged in to npm on your local machine:
```bash
npm login
```
3. You need to have all the necessary dependencies installed:
```bash
npm install
```
## Before Publishing
1. Make sure all tests pass:
```bash
npm test
```
2. Build all the necessary files:
```bash
npm run build # Build for Bun
npm run build:node # Build for Node.js
npm run build:stdio # Build the stdio server
```
3. Update the version number in `package.json` following [semantic versioning](https://semver.org/):
- MAJOR version for incompatible API changes
- MINOR version for new functionality in a backward-compatible manner
- PATCH version for backward-compatible bug fixes
4. Update the CHANGELOG.md file with the changes in the new version.
## Publishing
1. Publish to npm:
```bash
npm publish
```
If you want to publish a beta version:
```bash
npm publish --tag beta
```
2. Verify the package is published:
```bash
npm view homeassistant-mcp
```
## After Publishing
1. Create a git tag for the version:
```bash
git tag -a v1.0.0 -m "Version 1.0.0"
git push origin v1.0.0
```
2. Create a GitHub release with the same version number and include the changelog.
## Testing the Published Package
To test the published package:
```bash
# Install globally
npm install -g homeassistant-mcp
# Run the MCP server
homeassistant-mcp
# Or use npx without installing
npx homeassistant-mcp
```
## Unpublishing
If you need to unpublish a version (only possible within 72 hours of publishing):
```bash
npm unpublish homeassistant-mcp@1.0.0
```
## Publishing a New Version
1. Update the version in package.json
2. Update CHANGELOG.md
3. Build all files
4. Run tests
5. Publish to npm
6. Create a git tag
7. Create a GitHub release

452
README.md
View File

@@ -1,3 +1,226 @@
# Home Assistant Model Context Protocol (MCP)
A standardized protocol for AI assistants to interact with Home Assistant, providing a secure, typed, and extensible interface for controlling smart home devices.
## Overview
The Model Context Protocol (MCP) server acts as a bridge between AI models (like Claude, GPT, etc.) and Home Assistant, enabling AI assistants to:
- Execute commands on Home Assistant devices
- Retrieve information about the smart home
- Stream responses for long-running operations
- Validate parameters and inputs
- Provide consistent error handling
## Features
- **Modular Architecture** - Clean separation between transport, middleware, and tools
- **Typed Interface** - Fully TypeScript typed for better developer experience
- **Multiple Transports**:
- **Standard I/O** (stdin/stdout) for CLI integration
- **HTTP/REST API** with Server-Sent Events support for streaming
- **Middleware System** - Validation, logging, timeout, and error handling
- **Built-in Tools**:
- Light control (brightness, color, etc.)
- Climate control (thermostats, HVAC)
- More to come...
- **Extensible Plugin System** - Easily add new tools and capabilities
- **Streaming Responses** - Support for long-running operations
- **Parameter Validation** - Using Zod schemas
- **Claude & Cursor Integration** - Ready-made utilities for AI assistants
## Getting Started
### Prerequisites
- Node.js 16+
- Home Assistant instance (or you can use the mock implementations for testing)
### Installation
```bash
# Clone the repository
git clone https://github.com/your-repo/homeassistant-mcp.git
# Install dependencies
cd homeassistant-mcp
npm install
# Build the project
npm run build
```
### Running the Server
```bash
# Start with standard I/O transport (for AI assistant integration)
npm start -- --stdio
# Start with HTTP transport (for API access)
npm start -- --http
# Start with both transports
npm start -- --stdio --http
```
### Configuration
Configure the server using environment variables or a `.env` file:
```dotenv
# Server configuration
PORT=3000
NODE_ENV=development
# Execution settings
EXECUTION_TIMEOUT=30000
STREAMING_ENABLED=true
# Transport settings
USE_STDIO_TRANSPORT=true
USE_HTTP_TRANSPORT=true
# Debug and logging
DEBUG_MODE=false
DEBUG_STDIO=false
DEBUG_HTTP=false
SILENT_STARTUP=false
# CORS settings
CORS_ORIGIN=*
```
## Architecture
The MCP server is built with a layered architecture:
1. **Transport Layer** - Handles communication protocols (stdio, HTTP)
2. **Middleware Layer** - Processes requests through a pipeline
3. **Tool Layer** - Implements specific functionality
4. **Resource Layer** - Manages stateful resources
### Tools
Tools are the primary way to add functionality to the MCP server. Each tool:
- Has a unique name
- Accepts typed parameters
- Returns typed results
- Can stream partial results
- Validates inputs and outputs
Example tool registration:
```typescript
import { LightsControlTool } from "./tools/homeassistant/lights.tool.js";
import { ClimateControlTool } from "./tools/homeassistant/climate.tool.js";
// Register tools
server.registerTool(new LightsControlTool());
server.registerTool(new ClimateControlTool());
```
### API
When running with HTTP transport, the server provides a JSON-RPC 2.0 API:
- `POST /api/mcp/jsonrpc` - Execute a tool
- `GET /api/mcp/stream` - Connect to SSE stream for real-time updates
- `GET /api/mcp/info` - Get server information
- `GET /health` - Health check endpoint
## Integration with AI Models
### Claude Integration
```typescript
import { createClaudeToolDefinitions } from "./mcp/index.js";
// Generate Claude-compatible tool definitions
const claudeTools = createClaudeToolDefinitions([
new LightsControlTool(),
new ClimateControlTool()
]);
// Use with Claude API
const messages = [
{ role: "user", content: "Turn on the lights in the living room" }
];
const response = await claude.messages.create({
model: "claude-3-opus-20240229",
messages,
tools: claudeTools
});
```
### Cursor Integration
To use the Home Assistant MCP server with Cursor, add the following to your `.cursor/config/config.json` file:
```json
{
"mcpServers": {
"homeassistant-mcp": {
"command": "bash",
"args": ["-c", "cd ${workspaceRoot} && bun run dist/index.js --stdio 2>/dev/null | grep -E '\\{\"jsonrpc\":\"2\\.0\"'"],
"env": {
"NODE_ENV": "development",
"USE_STDIO_TRANSPORT": "true",
"DEBUG_STDIO": "true"
}
}
}
}
```
This configuration:
1. Runs the MCP server with stdio transport
2. Redirects all stderr output to /dev/null
3. Uses grep to filter stdout for lines containing `{"jsonrpc":"2.0"`, ensuring clean JSON-RPC output
#### Troubleshooting Cursor Integration
If you encounter a "failed to create client" error when using the MCP server with Cursor:
1. Make sure you're using the correct command and arguments in your Cursor configuration
- The bash script approach ensures only valid JSON-RPC messages reach Cursor
- Ensure the server is built by running `bun run build` before trying to connect
2. Ensure the server is properly outputting JSON-RPC messages to stdout:
```bash
bun run dist/index.js --stdio 2>/dev/null | grep -E '\{"jsonrpc":"2\.0"' > json_only.txt
```
Then examine json_only.txt to verify it contains only valid JSON-RPC messages.
3. Make sure grep is installed on your system (it should be available by default on most systems)
4. Try rebuilding the server with:
```bash
bun run build
```
5. Enable debug mode by setting `DEBUG_STDIO=true` in the environment variables
If the issue persists, you can try:
1. Restarting Cursor
2. Clearing Cursor's cache (Help > Developer > Clear Cache and Reload)
3. Using a similar approach with Node.js:
```json
{
"command": "bash",
"args": ["-c", "cd ${workspaceRoot} && node dist/index.js --stdio 2>/dev/null | grep -E '\\{\"jsonrpc\":\"2\\.0\"'"]
}
```
## License
MIT
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
# MCP Server for Home Assistant 🏠🤖
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Bun](https://img.shields.io/badge/bun-%3E%3D1.0.26-black)](https://bun.sh) [![TypeScript](https://img.shields.io/badge/typescript-%5E5.0.0-blue.svg)](https://www.typescriptlang.org) [![smithery badge](https://smithery.ai/badge/@jango-blockchained/advanced-homeassistant-mcp)](https://smithery.ai/server/@jango-blockchained/advanced-homeassistant-mcp)
@@ -12,6 +235,7 @@ MCP (Model Context Protocol) Server is my lightweight integration tool for Home
- 📡 WebSocket/Server-Sent Events (SSE) for state updates
- 🤖 Simple automation rule management
- 🔐 JWT-based authentication
- 🔄 Standard I/O (stdio) transport for integration with Claude and other AI assistants
## Why Bun? 🚀
@@ -201,11 +425,12 @@ Add to `.cursor/config/config.json`:
{
"mcpServers": {
"homeassistant-mcp": {
"command": "bun",
"args": ["run", "start"],
"cwd": "${workspaceRoot}",
"command": "bash",
"args": ["-c", "cd ${workspaceRoot} && bun run dist/index.js --stdio 2>/dev/null | grep -E '\\{\"jsonrpc\":\"2\\.0\"'"],
"env": {
"NODE_ENV": "development"
"NODE_ENV": "development",
"USE_STDIO_TRANSPORT": "true",
"DEBUG_STDIO": "true"
}
}
}
@@ -317,3 +542,222 @@ MIT License. See [LICENSE](LICENSE) for details.
## Author 👨‍💻
Created by [jango-blockchained](https://github.com/jango-blockchained)
## Running with Standard I/O Transport 📝
MCP Server supports a JSON-RPC 2.0 stdio transport mode for direct integration with AI assistants like Claude:
### MCP Stdio Features
✅ **JSON-RPC 2.0 Compatibility**: Full support for the MCP protocol standard
✅ **NPX Support**: Run directly without installation using `npx homeassistant-mcp`
✅ **Auto Configuration**: Creates necessary directories and default configuration
✅ **Cross-Platform**: Works on macOS, Linux, and Windows
✅ **Claude Desktop Integration**: Ready to use with Claude Desktop
✅ **Parameter Validation**: Automatic validation of tool parameters
✅ **Error Handling**: Standardized error codes and handling
✅ **Detailed Logging**: Logs to files without polluting stdio
### Option 1: Using NPX (Easiest)
Run the MCP server directly without installation using npx:
```bash
# Basic usage
npx homeassistant-mcp
# Or with environment variables
HASS_URL=http://your-ha-instance:8123 HASS_TOKEN=your_token npx homeassistant-mcp
```
This will:
1. Install the package temporarily
2. Automatically run in stdio mode with JSON-RPC 2.0 transport
3. Create a logs directory for logging
4. Create a default .env file if not present
Perfect for integration with Claude Desktop or other MCP clients.
#### Integrating with Claude Desktop
To use MCP with Claude Desktop:
1. Open Claude Desktop settings
2. Go to the "Advanced" tab
3. Under "MCP Server", select "Custom"
4. Enter the command: `npx homeassistant-mcp`
5. Click "Save"
Claude will now use the MCP server for Home Assistant integration, allowing you to control your smart home directly through Claude.
### Option 2: Local Installation
1. Update your `.env` file to enable stdio transport:
```
USE_STDIO_TRANSPORT=true
```
2. Run the server using the stdio-start script:
```bash
./stdio-start.sh
```
Available options:
```
./stdio-start.sh --debug # Enable debug mode
./stdio-start.sh --rebuild # Force rebuild
./stdio-start.sh --help # Show help
```
When running in stdio mode:
- The server communicates via stdin/stdout using JSON-RPC 2.0 format
- No HTTP server is started
- Console logging is disabled to avoid polluting the stdio stream
- All logs are written to the log files in the `logs/` directory
### JSON-RPC 2.0 Message Format
#### Request Format
```json
{
"jsonrpc": "2.0",
"id": "unique-request-id",
"method": "tool-name",
"params": {
"param1": "value1",
"param2": "value2"
}
}
```
#### Response Format
```json
{
"jsonrpc": "2.0",
"id": "unique-request-id",
"result": {
// Tool-specific result data
}
}
```
#### Error Response Format
```json
{
"jsonrpc": "2.0",
"id": "unique-request-id",
"error": {
"code": -32000,
"message": "Error message",
"data": {} // Optional error details
}
}
```
#### Notification Format (Server to Client)
```json
{
"jsonrpc": "2.0",
"method": "notification-type",
"params": {
// Notification data
}
}
```
### Supported Error Codes
| Code | Description | Meaning |
|---------|--------------------|------------------------------------------|
| -32700 | Parse error | Invalid JSON was received |
| -32600 | Invalid request | JSON is not a valid request object |
| -32601 | Method not found | Method does not exist or is unavailable |
| -32602 | Invalid params | Invalid method parameters |
| -32603 | Internal error | Internal JSON-RPC error |
| -32000 | Tool execution | Error executing the tool |
| -32001 | Validation error | Parameter validation failed |
### Integrating with Claude Desktop
To use this MCP server with Claude Desktop:
1. Create or edit your Claude Desktop configuration:
```bash
# On macOS
nano ~/Library/Application\ Support/Claude/claude_desktop_config.json
# On Linux
nano ~/.config/Claude/claude_desktop_config.json
# On Windows
notepad %APPDATA%\Claude\claude_desktop_config.json
```
2. Add the MCP server configuration:
```json
{
"mcpServers": {
"homeassistant-mcp": {
"command": "npx",
"args": ["homeassistant-mcp"],
"env": {
"HASS_TOKEN": "your_home_assistant_token_here",
"HASS_HOST": "http://your_home_assistant_host:8123"
}
}
}
}
```
3. Restart Claude Desktop.
4. In Claude, you can now use the Home Assistant MCP tools.
### JSON-RPC 2.0 Message Format
## Usage
### Using NPX (Easiest)
The simplest way to use the Home Assistant MCP server is through NPX:
```bash
# Start the server in stdio mode
npx homeassistant-mcp
```
This will automatically:
1. Start the server in stdio mode
2. Output JSON-RPC messages to stdout
3. Send log messages to stderr
4. Create a logs directory if it doesn't exist
You can redirect stderr to hide logs and only see the JSON-RPC output:
```bash
npx homeassistant-mcp 2>/dev/null
```
### Manual Installation
If you prefer to install the package globally or locally:
```bash
# Install globally
npm install -g homeassistant-mcp
# Then run
homeassistant-mcp
```
Or install locally:
```bash
# Install locally
npm install homeassistant-mcp
# Then run using npx
npx homeassistant-mcp
```
### Advanced Usage

84
bin/mcp-stdio.cjs Executable file
View File

@@ -0,0 +1,84 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const dotenv = require('dotenv');
/**
* MCP Server - Stdio Transport Mode (CommonJS)
*
* This is the CommonJS entry point for running the MCP server via NPX in stdio mode.
* It will directly load the stdio-server.js file which is optimized for the CLI usage.
*/
// Set environment variable for stdio transport
process.env.USE_STDIO_TRANSPORT = 'true';
// Load environment variables from .env file (if exists)
try {
const envPath = path.resolve(process.cwd(), '.env');
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
} else {
// Load .env.example if it exists
const examplePath = path.resolve(process.cwd(), '.env.example');
if (fs.existsSync(examplePath)) {
dotenv.config({ path: examplePath });
}
}
} catch (error) {
// Silent error handling
}
// Ensure logs directory exists
try {
const logsDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
} catch (error) {
// Silent error handling
}
// Try to load the server
try {
// Check for simplified stdio server build first (preferred for CLI usage)
const stdioServerPath = path.resolve(__dirname, '../dist/stdio-server.js');
if (fs.existsSync(stdioServerPath)) {
// If we're running in Node.js (not Bun), we need to handle ESM imports differently
if (typeof Bun === 'undefined') {
// Use dynamic import for ESM modules in CommonJS
import(stdioServerPath).catch((err) => {
console.error('Failed to import stdio server:', err.message);
process.exit(1);
});
} else {
// In Bun, we can directly require the module
require(stdioServerPath);
}
} else {
// Fall back to full server if available
const fullServerPath = path.resolve(__dirname, '../dist/index.js');
if (fs.existsSync(fullServerPath)) {
console.warn('Warning: stdio-server.js not found, falling back to index.js');
console.warn('For optimal CLI performance, build with "npm run build:stdio"');
if (typeof Bun === 'undefined') {
import(fullServerPath).catch((err) => {
console.error('Failed to import server:', err.message);
process.exit(1);
});
} else {
require(fullServerPath);
}
} else {
console.error('Error: No server implementation found. Please build the project first.');
process.exit(1);
}
}
} catch (error) {
console.error('Error starting server:', error.message);
process.exit(1);
}

41
bin/mcp-stdio.js Executable file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env node
/**
* MCP Server - Stdio Transport Mode
*
* This is the entry point for running the MCP server via NPX in stdio mode.
* It automatically configures the server to use JSON-RPC 2.0 over stdin/stdout.
*/
// Set environment variables for stdio transport
process.env.USE_STDIO_TRANSPORT = 'true';
// Import and run the MCP server from the compiled output
try {
// First make sure required directories exist
const fs = require('fs');
const path = require('path');
// Ensure logs directory exists
const logsDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logsDir)) {
console.error('Creating logs directory...');
fs.mkdirSync(logsDir, { recursive: true });
}
// Get the entry module path
const entryPath = require.resolve('../dist/index.js');
// Print initial message to stderr
console.error('Starting MCP server in stdio transport mode...');
console.error('Logs will be written to the logs/ directory');
console.error('Communication will use JSON-RPC 2.0 format via stdin/stdout');
// Run the server
require(entryPath);
} catch (error) {
console.error('Failed to start MCP server:', error.message);
console.error('If this is your first run, you may need to build the project first:');
console.error(' npm run build');
process.exit(1);
}

83
bin/npx-entry.cjs Executable file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
// Set environment variable - enable stdio transport and silence output
process.env.USE_STDIO_TRANSPORT = 'true';
process.env.LOG_LEVEL = 'silent';
// Ensure logs directory exists
const logsDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
// Check if .env exists, create from example if not
const envPath = path.join(process.cwd(), '.env');
const envExamplePath = path.join(process.cwd(), '.env.example');
if (!fs.existsSync(envPath) && fs.existsSync(envExamplePath)) {
fs.copyFileSync(envExamplePath, envPath);
}
// Start the MCP server with redirected stderr
try {
// Use our silent-mcp.sh script if it exists, otherwise use mcp-stdio.cjs
const silentScriptPath = path.join(process.cwd(), 'silent-mcp.sh');
if (fs.existsSync(silentScriptPath) && fs.statSync(silentScriptPath).isFile()) {
// Execute the silent-mcp.sh script instead
const childProcess = spawn('/bin/bash', [silentScriptPath], {
stdio: ['inherit', 'inherit', 'ignore'], // Redirect stderr to /dev/null
});
childProcess.on('error', (err) => {
console.error('Failed to start server:', err.message);
process.exit(1);
});
// Properly handle process termination
process.on('SIGINT', () => {
childProcess.kill('SIGINT');
});
process.on('SIGTERM', () => {
childProcess.kill('SIGTERM');
});
} else {
// Fall back to original method if silent-mcp.sh doesn't exist
const scriptPath = path.join(__dirname, 'mcp-stdio.cjs');
// Use 'pipe' for stdout and ignore (null) for stderr
const childProcess = spawn('node', [scriptPath], {
stdio: ['inherit', 'pipe', 'ignore'], // Redirect stderr to /dev/null
env: {
...process.env,
USE_STDIO_TRANSPORT: 'true',
LOG_LEVEL: 'silent'
}
});
// Pipe child's stdout to parent's stdout
childProcess.stdout.pipe(process.stdout);
childProcess.on('error', (err) => {
console.error('Failed to start server:', err.message);
process.exit(1);
});
// Properly handle process termination
process.on('SIGINT', () => {
childProcess.kill('SIGINT');
});
process.on('SIGTERM', () => {
childProcess.kill('SIGTERM');
});
}
} catch (error) {
console.error('Error starting server:', error.message);
process.exit(1);
}

62
bin/test-stdio.js Executable file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env node
/**
* Test script for MCP stdio transport
*
* This script sends JSON-RPC 2.0 requests to the MCP server
* running in stdio mode and displays the responses.
*
* Usage: node test-stdio.js | node bin/mcp-stdio.cjs
*/
// Send a ping request
const pingRequest = {
jsonrpc: "2.0",
id: 1,
method: "ping"
};
// Send an info request
const infoRequest = {
jsonrpc: "2.0",
id: 2,
method: "info"
};
// Send an echo request
const echoRequest = {
jsonrpc: "2.0",
id: 3,
method: "echo",
params: {
message: "Hello, MCP!",
timestamp: new Date().toISOString(),
test: true,
count: 42
}
};
// Send the requests with a delay between them
setTimeout(() => {
console.log(JSON.stringify(pingRequest));
}, 500);
setTimeout(() => {
console.log(JSON.stringify(infoRequest));
}, 1000);
setTimeout(() => {
console.log(JSON.stringify(echoRequest));
}, 1500);
// Process responses
process.stdin.on('data', (data) => {
try {
const response = JSON.parse(data.toString());
console.error('Received response:');
console.error(JSON.stringify(response, null, 2));
} catch (error) {
console.error('Error parsing response:', error);
console.error('Raw data:', data.toString());
}
});

309
bun.lock
View File

@@ -12,8 +12,10 @@
"@types/ws": "^8.5.10",
"@xmldom/xmldom": "^0.9.7",
"chalk": "^5.4.1",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"elysia": "^1.2.11",
"express": "^4.21.2",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"node-fetch": "^3.3.2",
@@ -29,11 +31,13 @@
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/bun": "latest",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"ajv": "^8.17.1",
"bun-types": "^1.2.2",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
@@ -41,7 +45,12 @@
"husky": "^9.0.11",
"prettier": "^3.2.5",
"supertest": "^6.3.3",
"terser-webpack-plugin": "^5.3.10",
"ts-loader": "^9.5.1",
"uuid": "^11.0.5",
"webpack": "^5.98.0",
"webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0",
},
},
},
@@ -118,6 +127,8 @@
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, ""],
"@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.7", "", {}, "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="],
"@elysiajs/cors": ["@elysiajs/cors@1.2.0", "", { "peerDependencies": { "elysia": ">= 1.2.0" } }, ""],
"@elysiajs/swagger": ["@elysiajs/swagger@1.2.0", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, ""],
@@ -162,6 +173,8 @@
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.6", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
@@ -192,6 +205,14 @@
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/cors": ["@types/cors@2.8.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA=="],
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
"@types/express": ["@types/express@5.0.0", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="],
@@ -208,6 +229,8 @@
"@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.8", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, ""],
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
@@ -260,17 +283,63 @@
"@unhead/schema": ["@unhead/schema@1.11.18", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, ""],
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
"@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
"@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="],
"@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="],
"@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="],
"@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="],
"@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="],
"@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="],
"@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="],
"@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="],
"@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="],
"@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="],
"@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="],
"@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="],
"@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
"@webpack-cli/configtest": ["@webpack-cli/configtest@2.1.1", "", { "peerDependencies": { "webpack": "5.x.x", "webpack-cli": "5.x.x" } }, "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw=="],
"@webpack-cli/info": ["@webpack-cli/info@2.0.2", "", { "peerDependencies": { "webpack": "5.x.x", "webpack-cli": "5.x.x" } }, "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A=="],
"@webpack-cli/serve": ["@webpack-cli/serve@2.0.5", "", { "peerDependencies": { "webpack": "5.x.x", "webpack-cli": "5.x.x" } }, "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.9.7", "", {}, ""],
"@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
"@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, ""],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"acorn": ["acorn@8.14.0", "", { "bin": "bin/acorn" }, ""],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, ""],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, ""],
"ajv": ["ajv@6.12.6", "", { "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" } }, ""],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, ""],
@@ -280,6 +349,8 @@
"argparse": ["argparse@2.0.1", "", {}, ""],
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
"array-union": ["array-union@2.1.0", "", {}, ""],
"asap": ["asap@2.0.6", "", {}, ""],
@@ -294,6 +365,8 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, ""],
"body-parser": ["body-parser@1.20.3", "", { "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" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, ""],
@@ -304,8 +377,12 @@
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, ""],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, ""],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, ""],
"call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, ""],
@@ -318,8 +395,12 @@
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
"chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
"ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
"clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="],
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, ""],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""],
@@ -328,20 +409,32 @@
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, ""],
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, ""],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, ""],
"commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
"component-emitter": ["component-emitter@1.3.1", "", {}, ""],
"concat-map": ["concat-map@0.0.1", "", {}, ""],
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@1.0.2", "", {}, ""],
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
"cookiejar": ["cookiejar@2.1.4", "", {}, ""],
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, ""],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, ""],
@@ -354,6 +447,10 @@
"delayed-stream": ["delayed-stream@1.0.0", "", {}, ""],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
"dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, ""],
"diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="],
@@ -376,22 +473,34 @@
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, ""],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"electron-to-chromium": ["electron-to-chromium@1.5.116", "", {}, "sha512-mufxTCJzLBQVvSdZzX1s5YAuXsN1M4tTyYxOOL1TcSKtIzQ9rjIrm7yFK80rN5dwGTePgdoABDSHpuVtRQh0Zw=="],
"elysia": ["elysia@1.2.12", "", { "dependencies": { "@sinclair/typebox": "^0.34.15", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" } }, ""],
"enabled": ["enabled@2.0.0", "", {}, ""],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
"entities": ["entities@4.5.0", "", {}, ""],
"envinfo": ["envinfo@7.14.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, ""],
"es-errors": ["es-errors@1.3.0", "", {}, ""],
"es-module-lexer": ["es-module-lexer@1.6.0", "", {}, "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, ""],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, ""],
"eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": "bin/eslint.js" }, ""],
@@ -416,10 +525,16 @@
"esutils": ["esutils@2.0.3", "", {}, ""],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, ""],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="],
"express": ["express@4.21.2", "", { "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" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, ""],
"fast-diff": ["fast-diff@1.3.0", "", {}, ""],
@@ -432,6 +547,10 @@
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, ""],
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
"fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="],
"fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, ""],
"fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="],
@@ -446,8 +565,12 @@
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, ""],
"finalhandler": ["finalhandler@1.3.1", "", { "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" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, ""],
"flat": ["flat@5.0.2", "", { "bin": { "flat": "cli.js" } }, "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="],
"flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, ""],
"flatted": ["flatted@3.3.2", "", {}, ""],
@@ -464,6 +587,10 @@
"formidable": ["formidable@2.1.2", "", { "dependencies": { "dezalgo": "^1.0.4", "hexoid": "^1.0.0", "once": "^1.4.0", "qs": "^6.11.0" } }, ""],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, ""],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -482,6 +609,8 @@
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, ""],
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
"globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, ""],
"globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, ""],
@@ -506,22 +635,34 @@
"htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, ""],
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, ""],
"husky": ["husky@9.1.7", "", { "bin": "bin.js" }, ""],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"ignore": ["ignore@5.3.2", "", {}, ""],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, ""],
"import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, ""],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, ""],
"inherits": ["inherits@2.0.4", "", {}, ""],
"interpret": ["interpret@3.1.1", "", {}, "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-arrayish": ["is-arrayish@0.3.2", "", {}, ""],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, ""],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, ""],
@@ -536,6 +677,8 @@
"isexe": ["isexe@2.0.0", "", {}, ""],
"isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="],
"istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
"istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="],
@@ -558,7 +701,7 @@
"jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="],
"jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="],
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@@ -568,7 +711,9 @@
"json-buffer": ["json-buffer@3.0.1", "", {}, ""],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, ""],
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, ""],
@@ -582,10 +727,14 @@
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, ""],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"kuler": ["kuler@2.0.0", "", {}, ""],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, ""],
"loader-runner": ["loader-runner@4.3.0", "", {}, "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, ""],
"lodash.includes": ["lodash.includes@4.3.0", "", {}, ""],
@@ -612,8 +761,12 @@
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, ""],
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
"memoirist": ["memoirist@0.3.0", "", {}, ""],
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"merge2": ["merge2@1.4.1", "", {}, ""],
@@ -638,6 +791,10 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, ""],
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, ""],
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, ""],
@@ -650,10 +807,14 @@
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-hash": ["object-hash@3.0.0", "", {}, ""],
"object-inspect": ["object-inspect@1.13.4", "", {}, ""],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, ""],
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, ""],
@@ -674,12 +835,18 @@
"parse-srcset": ["parse-srcset@1.0.2", "", {}, ""],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-exists": ["path-exists@4.0.0", "", {}, ""],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, ""],
"path-key": ["path-key@3.1.1", "", {}, ""],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
"path-type": ["path-type@4.0.0", "", {}, ""],
"pathe": ["pathe@1.1.2", "", {}, ""],
@@ -690,6 +857,8 @@
"pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="],
"pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="],
"postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, ""],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, ""],
@@ -700,16 +869,32 @@
"pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"punycode": ["punycode@2.3.1", "", {}, ""],
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, ""],
"qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, ""],
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, ""],
"rechoir": ["rechoir@0.8.0", "", { "dependencies": { "resolve": "^1.20.0" } }, "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
"resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, ""],
"reusify": ["reusify@1.0.4", "", {}, ""],
@@ -722,10 +907,24 @@
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, ""],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sanitize-html": ["sanitize-html@2.14.0", "", { "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", "htmlparser2": "^8.0.0", "is-plain-object": "^5.0.0", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, ""],
"schema-utils": ["schema-utils@4.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g=="],
"semver": ["semver@7.7.1", "", { "bin": "bin/semver.js" }, ""],
"send": ["send@0.19.0", "", { "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" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
"serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="],
"serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shallow-clone": ["shallow-clone@3.0.1", "", { "dependencies": { "kind-of": "^6.0.2" } }, "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, ""],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, ""],
@@ -744,14 +943,20 @@
"slash": ["slash@3.0.0", "", {}, ""],
"source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, ""],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"stack-trace": ["stack-trace@0.0.10", "", {}, ""],
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, ""],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, ""],
@@ -764,8 +969,16 @@
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, ""],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, ""],
"tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
"terser": ["terser@5.39.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw=="],
"terser-webpack-plugin": ["terser-webpack-plugin@5.3.14", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw=="],
"test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="],
"text-hex": ["text-hex@1.0.0", "", {}, ""],
@@ -776,12 +989,16 @@
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, ""],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"tr46": ["tr46@0.0.3", "", {}, ""],
"triple-beam": ["triple-beam@1.4.1", "", {}, ""],
"ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, ""],
"ts-loader": ["ts-loader@9.5.2", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", "micromatch": "^4.0.0", "semver": "^7.3.4", "source-map": "^0.7.4" }, "peerDependencies": { "typescript": "*", "webpack": "^5.0.0" } }, "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw=="],
"tslib": ["tslib@2.8.1", "", {}, ""],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, ""],
@@ -790,28 +1007,50 @@
"type-fest": ["type-fest@0.20.2", "", {}, ""],
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""],
"undici-types": ["undici-types@6.19.8", "", {}, ""],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, ""],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, ""],
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"uuid": ["uuid@11.0.5", "", { "bin": "dist/esm/bin/uuid" }, ""],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="],
"watchpack": ["watchpack@2.4.2", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, ""],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, ""],
"webpack": ["webpack@5.98.0", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA=="],
"webpack-cli": ["webpack-cli@5.1.4", "", { "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", "@webpack-cli/info": "^2.0.2", "@webpack-cli/serve": "^2.0.5", "colorette": "^2.0.14", "commander": "^10.0.1", "cross-spawn": "^7.0.3", "envinfo": "^7.7.3", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^3.1.1", "rechoir": "^0.8.0", "webpack-merge": "^5.7.3" }, "peerDependencies": { "webpack": "5.x.x" }, "bin": { "webpack-cli": "bin/cli.js" } }, "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg=="],
"webpack-merge": ["webpack-merge@5.10.0", "", { "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", "wildcard": "^2.0.0" } }, "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA=="],
"webpack-node-externals": ["webpack-node-externals@3.0.0", "", {}, "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ=="],
"webpack-sources": ["webpack-sources@3.2.3", "", {}, "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, ""],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, ""],
"wildcard": ["wildcard@2.0.1", "", {}, "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ=="],
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, ""],
"winston-daily-rotate-file": ["winston-daily-rotate-file@5.0.0", "", { "dependencies": { "file-stream-rotator": "^0.6.1", "object-hash": "^3.0.0", "triple-beam": "^1.4.1", "winston-transport": "^4.7.0" }, "peerDependencies": { "winston": "^3" } }, ""],
@@ -840,6 +1079,8 @@
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@eslint/eslintrc/ajv": ["ajv@6.12.6", "", { "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" } }, ""],
"@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
@@ -856,18 +1097,34 @@
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, ""],
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"clone-deep/is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="],
"color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, ""],
"eslint/ajv": ["ajv@6.12.6", "", { "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" } }, ""],
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""],
"express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, ""],
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"formdata-node/web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, ""],
"formidable/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, ""],
"istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"jest-diff/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""],
"jest-haste-map/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="],
"jest-matcher-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""],
"jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""],
@@ -884,8 +1141,30 @@
"openai/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, ""],
"pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"resolve-cwd/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
"superagent/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, ""],
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"ts-loader/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""],
"webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
"@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, ""],
"@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
@@ -898,12 +1177,22 @@
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, ""],
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"color/color-convert/color-name": ["color-name@1.1.3", "", {}, ""],
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, ""],
"eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""],
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"jest-diff/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""],
"jest-haste-map/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"jest-matcher-utils/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""],
"jest-message-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""],
@@ -916,8 +1205,20 @@
"openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, ""],
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"ts-loader/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""],
"webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
}
}

View File

@@ -4,10 +4,19 @@
"description": "Home Assistant Model Context Protocol",
"main": "dist/index.js",
"type": "module",
"bin": {
"homeassistant-mcp": "./bin/npx-entry.cjs",
"mcp-stdio": "./bin/npx-entry.cjs"
},
"scripts": {
"start": "bun run dist/index.js",
"start:stdio": "bun run dist/stdio-server.js",
"dev": "bun --hot --watch src/index.ts",
"build": "bun build ./src/index.ts --outdir ./dist --target bun --minify",
"build:node": "webpack --config webpack.config.cjs",
"build:stdio": "bun build ./src/stdio-server.ts --outdir ./dist --target node --minify",
"prepare": "husky install && npm run build",
"stdio": "node ./bin/mcp-stdio.js",
"test": "bun test",
"test:watch": "bun test --watch",
"test:coverage": "bun test --coverage",
@@ -17,7 +26,6 @@
"test:staged": "bun test --findRelatedTests",
"lint": "eslint . --ext .ts",
"format": "prettier --write \"src/**/*.ts\"",
"prepare": "husky install",
"profile": "bun --inspect src/index.ts",
"clean": "rm -rf dist .bun coverage",
"typecheck": "bun x tsc --noEmit",
@@ -32,8 +40,10 @@
"@types/ws": "^8.5.10",
"@xmldom/xmldom": "^0.9.7",
"chalk": "^5.4.1",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"elysia": "^1.2.11",
"express": "^4.21.2",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"node-fetch": "^3.3.2",
@@ -49,11 +59,13 @@
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/bun": "latest",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"ajv": "^8.17.1",
"bun-types": "^1.2.2",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
@@ -61,9 +73,24 @@
"husky": "^9.0.11",
"prettier": "^3.2.5",
"supertest": "^6.3.3",
"uuid": "^11.0.5"
"terser-webpack-plugin": "^5.3.10",
"ts-loader": "^9.5.1",
"uuid": "^11.0.5",
"webpack": "^5.98.0",
"webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0"
},
"engines": {
"bun": ">=1.0.0"
}
"bun": ">=1.0.0",
"node": ">=18.0.0"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist",
"bin",
"README.md",
"LICENSE"
]
}

14
silent-mcp.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
# Set silent environment variables
export LOG_LEVEL=silent
export USE_STDIO_TRANSPORT=true
# Check if we're running from npx or directly
if [ -f "./dist/stdio-server.js" ]; then
# Direct run from project directory - use local file
node ./dist/stdio-server.js 2>/dev/null
else
# Run using npx
npx homeassistant-mcp 2>/dev/null
fi

32
src/config.js Normal file
View File

@@ -0,0 +1,32 @@
/**
* MCP Server Configuration
*
* This file contains the configuration for the MCP server.
* Values can be overridden via environment variables.
*/
// Default values for the application configuration
export const APP_CONFIG = {
// Server configuration
PORT: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
NODE_ENV: process.env.NODE_ENV || 'development',
// Execution settings
EXECUTION_TIMEOUT: process.env.EXECUTION_TIMEOUT ? parseInt(process.env.EXECUTION_TIMEOUT, 10) : 30000,
STREAMING_ENABLED: process.env.STREAMING_ENABLED === 'true',
// Transport settings
USE_STDIO_TRANSPORT: process.env.USE_STDIO_TRANSPORT === 'true',
USE_HTTP_TRANSPORT: process.env.USE_HTTP_TRANSPORT !== 'false',
// Debug and logging settings
DEBUG_MODE: process.env.DEBUG_MODE === 'true',
DEBUG_STDIO: process.env.DEBUG_STDIO === 'true',
DEBUG_HTTP: process.env.DEBUG_HTTP === 'true',
SILENT_STARTUP: process.env.SILENT_STARTUP === 'true',
// CORS settings
CORS_ORIGIN: process.env.CORS_ORIGIN || '*'
};
export default APP_CONFIG;

52
src/config.ts Normal file
View File

@@ -0,0 +1,52 @@
/**
* Configuration for the Model Context Protocol (MCP) server
* Values can be overridden using environment variables
*/
export interface MCPServerConfig {
// Server configuration
port: number;
environment: string;
// Execution settings
executionTimeout: number;
streamingEnabled: boolean;
// Transport settings
useStdioTransport: boolean;
useHttpTransport: boolean;
// Debug and logging
debugMode: boolean;
debugStdio: boolean;
debugHttp: boolean;
silentStartup: boolean;
// CORS settings
corsOrigin: string;
}
export const APP_CONFIG: MCPServerConfig = {
// Server configuration
port: parseInt(process.env.PORT || '3000', 10),
environment: process.env.NODE_ENV || 'development',
// Execution settings
executionTimeout: parseInt(process.env.EXECUTION_TIMEOUT || '30000', 10),
streamingEnabled: process.env.STREAMING_ENABLED === 'true',
// Transport settings
useStdioTransport: process.env.USE_STDIO_TRANSPORT === 'true',
useHttpTransport: process.env.USE_HTTP_TRANSPORT === 'true',
// Debug and logging
debugMode: process.env.DEBUG_MODE === 'true',
debugStdio: process.env.DEBUG_STDIO === 'true',
debugHttp: process.env.DEBUG_HTTP === 'true',
silentStartup: process.env.SILENT_STARTUP === 'true',
// CORS settings
corsOrigin: process.env.CORS_ORIGIN || '*',
};
export default APP_CONFIG;

View File

@@ -1,157 +1,153 @@
import { file } from "bun";
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { swagger } from "@elysiajs/swagger";
import {
rateLimiter,
securityHeaders,
validateRequest,
sanitizeInput,
errorHandler,
} from "./security/index.js";
import {
get_hass,
call_service,
list_devices,
get_states,
get_state,
} from "./hass/index.js";
import { z } from "zod";
import {
commonCommands,
coverCommands,
climateCommands,
type Command,
} from "./commands.js";
import { speechService } from "./speech/index.js";
import { APP_CONFIG } from "./config/app.config.js";
import { loadEnvironmentVariables } from "./config/loadEnv.js";
import { MCP_SCHEMA } from "./mcp/schema.js";
import {
listDevicesTool,
controlTool,
subscribeEventsTool,
getSSEStatsTool,
automationConfigTool,
addonTool,
packageTool,
sceneTool,
notifyTool,
historyTool,
} from "./tools/index.js";
/**
* Home Assistant Model Context Protocol (MCP) Server
* A standardized protocol for AI tools to interact with Home Assistant
*/
// Load environment variables based on NODE_ENV
await loadEnvironmentVariables();
import express from 'express';
import cors from 'cors';
import { MCPServer } from './mcp/MCPServer.js';
import { loggingMiddleware, timeoutMiddleware } from './mcp/middleware/index.js';
import { StdioTransport } from './mcp/transports/stdio.transport.js';
import { HttpTransport } from './mcp/transports/http.transport.js';
import { APP_CONFIG } from './config.js';
import { logger } from "./utils/logger.js";
// Configuration
const HASS_TOKEN = process.env.HASS_TOKEN;
const PORT = parseInt(process.env.PORT || "4000", 10);
// Home Assistant tools
import { LightsControlTool } from './tools/homeassistant/lights.tool.js';
import { ClimateControlTool } from './tools/homeassistant/climate.tool.js';
console.log("Initializing Home Assistant connection...");
// Home Assistant optional tools - these can be added as needed
// import { ControlTool } from './tools/control.tool.js';
// import { SceneTool } from './tools/scene.tool.js';
// import { AutomationTool } from './tools/automation.tool.js';
// import { NotifyTool } from './tools/notify.tool.js';
// import { ListDevicesTool } from './tools/list-devices.tool.js';
// import { HistoryTool } from './tools/history.tool.js';
// Define Tool interface and export it
export interface Tool {
name: string;
description: string;
parameters: z.ZodType<any>;
execute: (params: any) => Promise<any>;
/**
* Check if running in stdio mode via command line args
*/
function isStdioMode(): boolean {
return process.argv.includes('--stdio');
}
// Array to store tools
const tools: Tool[] = [
listDevicesTool,
controlTool,
subscribeEventsTool,
getSSEStatsTool,
automationConfigTool,
addonTool,
packageTool,
sceneTool,
notifyTool,
historyTool,
];
/**
* Main function to start the MCP server
*/
async function main() {
logger.info('Starting Home Assistant MCP Server...');
// Initialize Elysia app with middleware
const app = new Elysia()
.use(cors())
.use(swagger())
.use(rateLimiter)
.use(securityHeaders)
.use(validateRequest)
.use(sanitizeInput)
.use(errorHandler);
// Check if we're in stdio mode from command line
const useStdio = isStdioMode() || APP_CONFIG.useStdioTransport;
// Mount API routes
app.get("/api/mcp/schema", () => MCP_SCHEMA);
// Configure server
const EXECUTION_TIMEOUT = APP_CONFIG.executionTimeout;
const STREAMING_ENABLED = APP_CONFIG.streamingEnabled;
app.post("/api/mcp/execute", async ({ body }: { body: { name: string; parameters: Record<string, unknown> } }) => {
const { name: toolName, parameters } = body;
const tool = tools.find((t) => t.name === toolName);
// Get the server instance (singleton)
const server = MCPServer.getInstance();
if (!tool) {
return {
success: false,
message: `Tool '${toolName}' not found`,
};
}
// Register Home Assistant tools
server.registerTool(new LightsControlTool());
server.registerTool(new ClimateControlTool());
try {
const result = await tool.execute(parameters);
return {
success: true,
result,
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : "Unknown error occurred",
};
}
});
// Add optional tools here as needed
// server.registerTool(new ControlTool());
// server.registerTool(new SceneTool());
// server.registerTool(new NotifyTool());
// server.registerTool(new ListDevicesTool());
// server.registerTool(new HistoryTool());
// Health check endpoint with MCP info
app.get("/api/mcp/health", () => ({
status: "ok",
timestamp: new Date().toISOString(),
version: "1.0.0",
mcp_version: "1.0",
supported_tools: tools.map(t => t.name),
speech_enabled: APP_CONFIG.SPEECH.ENABLED,
wake_word_enabled: APP_CONFIG.SPEECH.WAKE_WORD_ENABLED,
speech_to_text_enabled: APP_CONFIG.SPEECH.SPEECH_TO_TEXT_ENABLED,
}));
// Add middlewares
server.use(loggingMiddleware);
server.use(timeoutMiddleware(EXECUTION_TIMEOUT));
// Initialize speech service if enabled
if (APP_CONFIG.SPEECH.ENABLED) {
console.log("Initializing speech service...");
speechService.initialize().catch((error) => {
console.error("Failed to initialize speech service:", error);
});
}
// Initialize transports
if (useStdio) {
logger.info('Using Standard I/O transport');
// Create API endpoints for each tool
tools.forEach((tool) => {
app.post(`/api/tools/${tool.name}`, async ({ body }: { body: Record<string, unknown> }) => {
const result = await tool.execute(body);
return result;
});
});
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
// Handle server shutdown
process.on("SIGTERM", async () => {
console.log("Received SIGTERM. Shutting down gracefully...");
if (APP_CONFIG.SPEECH.ENABLED) {
await speechService.shutdown().catch((error) => {
console.error("Error shutting down speech service:", error);
// Create and configure the stdio transport with debug enabled for stdio mode
const stdioTransport = new StdioTransport({
debug: true, // Always enable debug in stdio mode for better visibility
silent: false // Never be silent in stdio mode
});
}
process.exit(0);
});
// Export tools for testing purposes
export { tools };
// Explicitly set the server reference to ensure access to tools
stdioTransport.setServer(server);
// Register the transport
server.registerTransport(stdioTransport);
// Special handling for stdio mode - don't start other transports
if (isStdioMode()) {
logger.info('Running in pure stdio mode (from CLI)');
// Start the server
await server.start();
logger.info('MCP Server started successfully');
// Handle shutdown
const shutdown = async () => {
logger.info('Shutting down MCP Server...');
try {
await server.shutdown();
logger.info('MCP Server shutdown complete');
process.exit(0);
} catch (error) {
logger.error('Error during shutdown:', error);
process.exit(1);
}
};
// Register shutdown handlers
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
// Exit the function early as we're in stdio-only mode
return;
}
}
// HTTP transport (only if not in pure stdio mode)
if (APP_CONFIG.useHttpTransport) {
logger.info('Using HTTP transport on port ' + APP_CONFIG.port);
const app = express();
app.use(cors({
origin: APP_CONFIG.corsOrigin
}));
const httpTransport = new HttpTransport({
port: APP_CONFIG.port,
corsOrigin: APP_CONFIG.corsOrigin,
apiPrefix: "/api/mcp",
debug: APP_CONFIG.debugHttp
});
server.registerTransport(httpTransport);
}
// Start the server
await server.start();
logger.info('MCP Server started successfully');
// Handle shutdown
const shutdown = async () => {
logger.info('Shutting down MCP Server...');
try {
await server.shutdown();
logger.info('MCP Server shutdown complete');
process.exit(0);
} catch (error) {
logger.error('Error during shutdown:', error);
process.exit(1);
}
};
// Register shutdown handlers
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
}
// Run the main function
main().catch(error => {
logger.error('Error starting MCP Server:', error);
process.exit(1);
});

105
src/mcp/BaseTool.ts Normal file
View File

@@ -0,0 +1,105 @@
/**
* Base Tool Implementation for MCP
*
* This base class provides the foundation for all tools in the MCP implementation,
* with typed parameters, validation, and error handling.
*/
import { z } from 'zod';
import { ToolDefinition, ToolMetadata, MCPResponseStream } from './types.js';
/**
* Configuration options for creating a tool
*/
export interface ToolOptions<P = unknown> {
name: string;
description: string;
version: string;
parameters?: z.ZodType<P>;
metadata?: ToolMetadata;
}
/**
* Base class for all MCP tools
*
* Provides:
* - Parameter validation with Zod
* - Error handling
* - Streaming support
* - Type safety
*/
export abstract class BaseTool<P = unknown, R = unknown> implements ToolDefinition {
public readonly name: string;
public readonly description: string;
public readonly parameters?: z.ZodType<P>;
public readonly metadata: ToolMetadata;
/**
* Create a new tool
*/
constructor(options: ToolOptions<P>) {
this.name = options.name;
this.description = options.description;
this.parameters = options.parameters;
this.metadata = {
version: options.version,
category: options.metadata?.category || 'general',
tags: options.metadata?.tags || [],
examples: options.metadata?.examples || [],
};
}
/**
* Execute the tool with the given parameters
*
* @param params The validated parameters for the tool
* @param stream Optional stream for sending partial results
* @returns The result of the tool execution
*/
abstract execute(params: P, stream?: MCPResponseStream): Promise<R>;
/**
* Get the parameter schema as JSON schema
*/
public getParameterSchema(): Record<string, unknown> | undefined {
if (!this.parameters) return undefined;
return this.parameters.isOptional()
? { type: 'object', properties: {} }
: this.parameters.shape;
}
/**
* Get tool definition for registration
*/
public getDefinition(): ToolDefinition {
return {
name: this.name,
description: this.description,
parameters: this.parameters,
metadata: this.metadata
};
}
/**
* Validate parameters against the schema
*
* @param params Parameters to validate
* @returns Validated parameters
* @throws Error if validation fails
*/
protected validateParams(params: unknown): P {
if (!this.parameters) {
return {} as P;
}
try {
return this.parameters.parse(params);
} catch (error) {
if (error instanceof z.ZodError) {
const issues = error.issues.map(issue => `${issue.path.join('.')}: ${issue.message}`).join(', ');
throw new Error(`Parameter validation failed: ${issues}`);
}
throw error;
}
}
}

453
src/mcp/MCPServer.ts Normal file
View File

@@ -0,0 +1,453 @@
/**
* MCPServer.ts
*
* Core implementation of the Model Context Protocol server.
* This class manages tool registration, execution, and resource handling
* while providing integration with various transport layers.
*/
import { EventEmitter } from "events";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { logger } from "../utils/logger.js";
// Error code enum to break circular dependency
export enum MCPErrorCode {
// Standard JSON-RPC 2.0 error codes
PARSE_ERROR = -32700,
INVALID_REQUEST = -32600,
METHOD_NOT_FOUND = -32601,
INVALID_PARAMS = -32602,
INTERNAL_ERROR = -32603,
// Custom MCP error codes
TOOL_EXECUTION_ERROR = -32000,
VALIDATION_ERROR = -32001,
RESOURCE_NOT_FOUND = -32002,
RESOURCE_BUSY = -32003,
TIMEOUT = -32004,
CANCELED = -32005,
AUTHENTICATION_ERROR = -32006,
AUTHORIZATION_ERROR = -32007,
TRANSPORT_ERROR = -32008,
STREAMING_ERROR = -32009
}
// Server events enum to break circular dependency
export enum MCPServerEvents {
STARTING = "starting",
STARTED = "started",
SHUTTING_DOWN = "shuttingDown",
SHUTDOWN = "shutdown",
REQUEST_RECEIVED = "requestReceived",
RESPONSE_SENT = "responseSent",
RESPONSE_ERROR = "responseError",
TOOL_REGISTERED = "toolRegistered",
TRANSPORT_REGISTERED = "transportRegistered",
CONFIG_UPDATED = "configUpdated"
}
// Forward declarations to break circular dependency
import type {
ToolDefinition,
MCPMiddleware,
MCPRequest,
MCPResponse,
MCPContext,
TransportLayer,
MCPConfig,
ResourceManager
} from "./types.js";
/**
* Main Model Context Protocol server class
*/
export class MCPServer extends EventEmitter {
private static instance: MCPServer;
private tools: Map<string, ToolDefinition> = new Map();
private middlewares: MCPMiddleware[] = [];
private transports: TransportLayer[] = [];
private resourceManager: ResourceManager;
private config: MCPConfig;
private resources: Map<string, Map<string, any>> = new Map();
/**
* Private constructor for singleton pattern
*/
private constructor(config: Partial<MCPConfig> = {}) {
super();
this.config = {
maxRetries: 3,
retryDelay: 1000,
executionTimeout: 30000,
streamingEnabled: true,
maxPayloadSize: 10 * 1024 * 1024, // 10MB
...config
};
this.resourceManager = {
acquire: this.acquireResource.bind(this),
release: this.releaseResource.bind(this),
list: this.listResources.bind(this)
};
// Initialize with default middlewares
this.use(this.validationMiddleware.bind(this));
this.use(this.errorHandlingMiddleware.bind(this));
logger.info("MCP Server initialized");
}
/**
* Get singleton instance
*/
public static getInstance(config?: Partial<MCPConfig>): MCPServer {
if (!MCPServer.instance) {
MCPServer.instance = new MCPServer(config);
} else if (config) {
MCPServer.instance.configure(config);
}
return MCPServer.instance;
}
/**
* Update server configuration
*/
public configure(config: Partial<MCPConfig>): void {
this.config = {
...this.config,
...config
};
logger.debug("MCP Server configuration updated", { config });
this.emit(MCPServerEvents.CONFIG_UPDATED, this.config);
}
/**
* Register a new tool with the server
*/
public registerTool(tool: ToolDefinition): void {
if (this.tools.has(tool.name)) {
logger.warn(`Tool '${tool.name}' is already registered. Overwriting.`);
}
this.tools.set(tool.name, tool);
logger.debug(`Tool '${tool.name}' registered`);
this.emit(MCPServerEvents.TOOL_REGISTERED, tool);
}
/**
* Register multiple tools at once
*/
public registerTools(tools: ToolDefinition[]): void {
tools.forEach(tool => this.registerTool(tool));
}
/**
* Get a tool by name
*/
public getTool(name: string): ToolDefinition | undefined {
return this.tools.get(name);
}
/**
* Get all registered tools
*/
public getAllTools(): ToolDefinition[] {
return Array.from(this.tools.values());
}
/**
* Register a transport layer
*/
public registerTransport(transport: TransportLayer): void {
this.transports.push(transport);
transport.initialize(this.handleRequest.bind(this));
logger.debug(`Transport '${transport.name}' registered`);
this.emit(MCPServerEvents.TRANSPORT_REGISTERED, transport);
}
/**
* Add a middleware to the pipeline
*/
public use(middleware: MCPMiddleware): void {
this.middlewares.push(middleware);
logger.debug("Middleware added");
}
/**
* Handle an incoming request through the middleware pipeline
*/
public async handleRequest(request: MCPRequest): Promise<MCPResponse> {
const context: MCPContext = {
requestId: request.id ?? uuidv4(),
startTime: Date.now(),
resourceManager: this.resourceManager,
tools: this.tools,
config: this.config,
logger: logger.child({ requestId: request.id }),
server: this,
state: new Map()
};
logger.debug(`Handling request: ${context.requestId}`, { method: request.method });
this.emit(MCPServerEvents.REQUEST_RECEIVED, request, context);
let index = 0;
const next = async (): Promise<MCPResponse> => {
if (index < this.middlewares.length) {
const middleware = this.middlewares[index++];
return middleware(request, context, next);
} else {
return this.executeRequest(request, context);
}
};
try {
const response = await next();
this.emit(MCPServerEvents.RESPONSE_SENT, response, context);
return response;
} catch (error) {
const errorResponse: MCPResponse = {
id: request.id,
error: {
code: MCPErrorCode.INTERNAL_ERROR,
message: error instanceof Error ? error.message : String(error)
}
};
this.emit(MCPServerEvents.RESPONSE_ERROR, errorResponse, context);
return errorResponse;
}
}
/**
* Execute a tool request after middleware processing
*/
private async executeRequest(request: MCPRequest, context: MCPContext): Promise<MCPResponse> {
const { method, params = {} } = request;
// Special case for internal context retrieval (used by transports for initialization)
if (method === "_internal_getContext") {
return {
id: request.id,
result: {
context: context,
tools: Array.from(this.tools.values()).map(tool => ({
name: tool.name,
description: tool.description,
metadata: tool.metadata
}))
}
};
}
const tool = this.tools.get(method);
if (!tool) {
return {
id: request.id,
error: {
code: MCPErrorCode.METHOD_NOT_FOUND,
message: `Method not found: ${method}`
}
};
}
try {
const result = await tool.execute(params, context);
return {
id: request.id,
result
};
} catch (error) {
logger.error(`Error executing tool ${method}:`, error);
return {
id: request.id,
error: {
code: MCPErrorCode.TOOL_EXECUTION_ERROR,
message: error instanceof Error ? error.message : String(error)
}
};
}
}
/**
* Validation middleware
*/
private async validationMiddleware(
request: MCPRequest,
context: MCPContext,
next: () => Promise<MCPResponse>
): Promise<MCPResponse> {
const { method, params = {} } = request;
const tool = this.tools.get(method);
if (!tool) {
return {
id: request.id,
error: {
code: MCPErrorCode.METHOD_NOT_FOUND,
message: `Method not found: ${method}`
}
};
}
if (tool.parameters && params) {
try {
// Validate parameters with the schema
const validParams = tool.parameters.parse(params);
// Update with validated params (which may include defaults)
request.params = validParams;
} catch (validationError) {
return {
id: request.id,
error: {
code: MCPErrorCode.INVALID_PARAMS,
message: "Invalid parameters",
data: validationError instanceof Error ? validationError.message : String(validationError)
}
};
}
}
return next();
}
/**
* Error handling middleware
*/
private async errorHandlingMiddleware(
request: MCPRequest,
context: MCPContext,
next: () => Promise<MCPResponse>
): Promise<MCPResponse> {
try {
return await next();
} catch (error) {
logger.error(`Uncaught error in request pipeline:`, error);
return {
id: request.id,
error: {
code: MCPErrorCode.INTERNAL_ERROR,
message: error instanceof Error ? error.message : "An unknown error occurred",
data: error instanceof Error ? { name: error.name, stack: error.stack } : undefined
}
};
}
}
/**
* Resource acquisition
*/
private async acquireResource(resourceType: string, resourceId: string, context: MCPContext): Promise<any> {
logger.debug(`Acquiring resource: ${resourceType}/${resourceId}`);
// Initialize resource type map if not exists
if (!this.resources.has(resourceType)) {
this.resources.set(resourceType, new Map());
}
const typeResources = this.resources.get(resourceType);
// Create resource if it doesn't exist
if (!typeResources.has(resourceId)) {
// Create a placeholder for the resource
const resourceData = {
id: resourceId,
type: resourceType,
createdAt: Date.now(),
data: {}
};
// Store the resource
typeResources.set(resourceId, resourceData);
// Log resource creation
await Promise.resolve(); // Add await to satisfy linter
logger.debug(`Created new resource: ${resourceType}/${resourceId}`);
return resourceData;
}
// Return existing resource
return typeResources.get(resourceId);
}
/**
* Resource release
*/
private async releaseResource(resourceType: string, resourceId: string, context: MCPContext): Promise<void> {
logger.debug(`Releasing resource: ${resourceType}/${resourceId}`);
// Check if type exists
if (!this.resources.has(resourceType)) {
return;
}
const typeResources = this.resources.get(resourceType);
// Remove resource if it exists
if (typeResources.has(resourceId)) {
await Promise.resolve(); // Add await to satisfy linter
typeResources.delete(resourceId);
logger.debug(`Released resource: ${resourceType}/${resourceId}`);
}
}
/**
* List available resources
*/
private async listResources(context: MCPContext, resourceType?: string): Promise<string[]> {
if (resourceType) {
logger.debug(`Listing resources of type ${resourceType}`);
if (!this.resources.has(resourceType)) {
return [];
}
await Promise.resolve(); // Add await to satisfy linter
return Array.from(this.resources.get(resourceType).keys());
} else {
logger.debug('Listing all resource types');
await Promise.resolve(); // Add await to satisfy linter
return Array.from(this.resources.keys());
}
}
/**
* Start the server
*/
public async start(): Promise<void> {
logger.info("Starting MCP Server");
this.emit(MCPServerEvents.STARTING);
// Start all transports
for (const transport of this.transports) {
await transport.start();
}
this.emit(MCPServerEvents.STARTED);
logger.info("MCP Server started");
}
/**
* Gracefully shut down the server
*/
public async shutdown(): Promise<void> {
logger.info("Shutting down MCP Server");
this.emit(MCPServerEvents.SHUTTING_DOWN);
// Stop all transports
for (const transport of this.transports) {
await transport.stop();
}
// Clear resources
this.tools.clear();
this.middlewares = [];
this.transports = [];
this.resources.clear();
this.emit(MCPServerEvents.SHUTDOWN);
this.removeAllListeners();
logger.info("MCP Server shut down");
}
}

153
src/mcp/index.ts Normal file
View File

@@ -0,0 +1,153 @@
/**
* MCP - Model Context Protocol Implementation
*
* This is the main entry point for the MCP implementation.
* It exports all the components needed to use the MCP.
*/
// Core MCP components
export * from './MCPServer.js';
export * from './types.js';
export * from './BaseTool.js';
// Middleware
export * from './middleware/index.js';
// Transports
export * from './transports/stdio.transport.js';
export * from './transports/http.transport.js';
// Utilities for AI assistants
export * from './utils/claude.js';
export * from './utils/cursor.js';
export * from './utils/error.js';
// Helper function to create Claude-compatible tool definitions
export function createClaudeToolDefinitions(tools: any[]): any[] {
return tools.map(tool => {
// Convert Zod schema to JSON Schema
const parameters = tool.parameters ? {
type: 'object',
properties: {},
required: []
} : {
type: 'object',
properties: {},
required: []
};
return {
name: tool.name,
description: tool.description,
parameters
};
});
}
// Helper function to create Cursor-compatible tool definitions
export function createCursorToolDefinitions(tools: any[]): any[] {
return tools.map(tool => {
// Convert to Cursor format
return {
name: tool.name,
description: tool.description,
parameters: {}
};
});
}
/**
* Model Context Protocol (MCP) Module
*
* This module provides the core MCP server implementation along with
* tools, transports, and utilities for integrating with Claude and Cursor.
*/
// Export server implementation
export { MCPServer } from "./MCPServer.js";
// Export type definitions
export * from "./types.js";
// Export transport layers
export { StdioTransport } from "./transports/stdio.transport.js";
// Re-export tools base class
export { BaseTool } from "../tools/base-tool.js";
// Re-export middleware
export * from "./middleware/index.js";
// Import types for proper type definitions
import { MCPServer } from "./MCPServer.js";
import { StdioTransport } from "./transports/stdio.transport.js";
import { ToolDefinition } from "./types.js";
/**
* Utility function to create Claude-compatible function definitions
*/
export function createClaudeFunctions(tools: ToolDefinition[]): any[] {
return tools.map(tool => {
// If the tool has a toSchemaObject method, use it
if ('toSchemaObject' in tool && typeof tool.toSchemaObject === 'function') {
return tool.toSchemaObject();
}
// Otherwise, manually convert the tool to a Claude function
return {
name: tool.name,
description: tool.description,
parameters: {
type: "object",
properties: (tool as any).parameters?.properties || {},
required: (tool as any).parameters?.required || []
}
};
});
}
/**
* Utility function to create Cursor-compatible tool definitions
*/
export function createCursorTools(tools: ToolDefinition[]): any[] {
return tools.map(tool => ({
name: tool.name,
description: tool.description,
parameters: Object.entries((tool as any).parameters?.properties || {}).reduce((acc, [key, value]) => {
const param = value as any;
acc[key] = {
type: param.type || 'string',
description: param.description || '',
required: ((tool as any).parameters?.required || []).includes(key)
};
return acc;
}, {} as Record<string, any>)
}));
}
/**
* Create a standalone MCP server with stdio transport
*/
export function createStdioServer(options: {
silent?: boolean;
debug?: boolean;
tools?: ToolDefinition[];
} = {}): { server: MCPServer; transport: StdioTransport } {
// Create server instance
const server = MCPServer.getInstance();
// Create and register stdio transport
const transport = new StdioTransport({
silent: options.silent,
debug: options.debug
});
server.registerTransport(transport);
// Register tools if provided
if (options.tools && Array.isArray(options.tools)) {
server.registerTools(options.tools);
}
return { server, transport };
}

172
src/mcp/middleware/index.ts Normal file
View File

@@ -0,0 +1,172 @@
/**
* MCP Middleware System
*
* This module provides middleware functionality for the MCP server,
* allowing for request/response processing pipelines.
*/
import { MCPMiddleware, MCPRequest, MCPResponse, MCPContext, MCPErrorCode } from "../types.js";
import { logger } from "../../utils/logger.js";
/**
* Middleware for validating requests against JSON Schema
*/
export const validationMiddleware: MCPMiddleware = async (
request: MCPRequest,
context: MCPContext,
next: () => Promise<MCPResponse>
): Promise<MCPResponse> => {
const { method } = request;
const tool = context.tools.get(method);
if (!tool) {
return {
id: request.id,
error: {
code: MCPErrorCode.METHOD_NOT_FOUND,
message: `Method not found: ${method}`
}
};
}
if (tool.parameters && request.params) {
try {
// Zod validation happens here
const validatedParams = tool.parameters.parse(request.params);
request.params = validatedParams;
} catch (error) {
return {
id: request.id,
error: {
code: MCPErrorCode.INVALID_PARAMS,
message: "Invalid parameters",
data: error instanceof Error ? error.message : String(error)
}
};
}
}
return next();
};
/**
* Middleware for handling authentication
*/
export const authMiddleware = (authKey: string): MCPMiddleware => {
return async (
request: MCPRequest,
context: MCPContext,
next: () => Promise<MCPResponse>
): Promise<MCPResponse> => {
// Check for authentication in params
const authToken = (request.params)?.auth_token;
if (!authToken || authToken !== authKey) {
return {
id: request.id,
error: {
code: MCPErrorCode.AUTHENTICATION_ERROR,
message: "Authentication failed"
}
};
}
// Remove auth token from params to keep them clean
if (request.params && typeof request.params === 'object') {
const { auth_token, ...cleanParams } = request.params;
request.params = cleanParams;
}
return next();
};
};
/**
* Middleware for logging requests and responses
*/
export const loggingMiddleware: MCPMiddleware = async (
request: MCPRequest,
context: MCPContext,
next: () => Promise<MCPResponse>
): Promise<MCPResponse> => {
const startTime = Date.now();
logger.debug(`MCP Request: ${request.method}`, {
id: request.id,
method: request.method
});
try {
const response = await next();
const duration = Date.now() - startTime;
logger.debug(`MCP Response: ${request.method}`, {
id: request.id,
method: request.method,
success: !response.error,
duration
});
return response;
} catch (error) {
const duration = Date.now() - startTime;
logger.error(`MCP Error: ${request.method}`, {
id: request.id,
method: request.method,
error,
duration
});
throw error;
}
};
/**
* Middleware for handling timeouts
*/
export const timeoutMiddleware = (timeoutMs: number): MCPMiddleware => {
return async (
request: MCPRequest,
context: MCPContext,
next: () => Promise<MCPResponse>
): Promise<MCPResponse> => {
return Promise.race([
next(),
new Promise<MCPResponse>((resolve) => {
setTimeout(() => {
resolve({
id: request.id,
error: {
code: MCPErrorCode.TIMEOUT,
message: `Request timed out after ${timeoutMs}ms`
}
});
}, timeoutMs);
})
]);
};
};
/**
* Utility to combine multiple middlewares
*/
export function combineMiddlewares(middlewares: MCPMiddleware[]): MCPMiddleware {
return async (
request: MCPRequest,
context: MCPContext,
next: () => Promise<MCPResponse>
): Promise<MCPResponse> => {
// Create a function that runs through all middlewares
let index = 0;
const runMiddleware = async (): Promise<MCPResponse> => {
if (index < middlewares.length) {
const middleware = middlewares[index++];
return middleware(request, context, runMiddleware);
} else {
return next();
}
};
return runMiddleware();
};
}

42
src/mcp/transport.ts Normal file
View File

@@ -0,0 +1,42 @@
/**
* Base Transport for MCP
*
* This module provides a base class for all transport implementations.
*/
import { TransportLayer, MCPRequest, MCPResponse, MCPStreamPart, MCPNotification } from "./types.js";
/**
* Abstract base class for all transports
*/
export abstract class BaseTransport implements TransportLayer {
public name: string = "base";
protected handler: ((request: MCPRequest) => Promise<MCPResponse>) | null = null;
/**
* Initialize the transport with a request handler
*/
public initialize(handler: (request: MCPRequest) => Promise<MCPResponse>): void {
this.handler = handler;
}
/**
* Start the transport
*/
public abstract start(): Promise<void>;
/**
* Stop the transport
*/
public abstract stop(): Promise<void>;
/**
* Send a notification to a client
*/
public sendNotification?(notification: MCPNotification): void;
/**
* Send a streaming response part
*/
public sendStreamPart?(streamPart: MCPStreamPart): void;
}

View File

@@ -0,0 +1,426 @@
/**
* HTTP Transport for MCP
*
* This module implements a JSON-RPC 2.0 transport layer over HTTP/HTTPS
* for the Model Context Protocol. It supports both traditional request/response
* patterns as well as streaming responses via Server-Sent Events (SSE).
*/
import { Server as HttpServer } from "http";
import express, { Express, Request, Response, NextFunction } from "express";
// Using a direct import now that we have the types
import cors from "cors";
import { TransportLayer, MCPRequest, MCPResponse, MCPStreamPart, MCPNotification, MCPErrorCode } from "../types.js";
import { logger } from "../../utils/logger.js";
import { EventEmitter } from "events";
type ServerSentEventsClient = {
id: string;
response: Response;
};
/**
* Implementation of TransportLayer using HTTP/Express
*/
export class HttpTransport implements TransportLayer {
public name = "http";
private handler: ((request: MCPRequest) => Promise<MCPResponse>) | null = null;
private app: Express;
private server: HttpServer | null = null;
private sseClients: Map<string, ServerSentEventsClient>;
private events: EventEmitter;
private initialized = false;
private port: number;
private corsOrigin: string | string[];
private apiPrefix: string;
private debug: boolean;
/**
* Constructor for HttpTransport
*/
constructor(options: {
port?: number;
corsOrigin?: string | string[];
apiPrefix?: string;
debug?: boolean;
} = {}) {
this.port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : 3000);
this.corsOrigin = options.corsOrigin ?? (process.env.CORS_ORIGIN || '*');
this.apiPrefix = options.apiPrefix ?? '/api';
this.debug = options.debug ?? (process.env.DEBUG_HTTP === "true");
this.app = express();
this.sseClients = new Map();
this.events = new EventEmitter();
// Configure max event listeners
this.events.setMaxListeners(100);
}
/**
* Initialize the transport with a request handler
*/
public initialize(handler: (request: MCPRequest) => Promise<MCPResponse>): void {
if (this.initialized) {
throw new Error("HttpTransport already initialized");
}
this.handler = handler;
this.initialized = true;
// Setup middleware
this.setupMiddleware();
// Setup routes
this.setupRoutes();
logger.info("HTTP transport initialized");
}
/**
* Setup Express middleware
*/
private setupMiddleware(): void {
// JSON body parser
this.app.use(express.json({ limit: '1mb' }));
// CORS configuration
// Using the imported cors middleware
try {
this.app.use(cors({
origin: this.corsOrigin,
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
} catch (err) {
logger.warn(`CORS middleware not available: ${String(err)}`);
}
// Request logging
if (this.debug) {
this.app.use((req, res, next) => {
logger.debug(`HTTP ${req.method} ${req.url}`);
next();
});
}
// Error handling middleware
this.app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error(`Express error: ${err.message}`);
res.status(500).json({
jsonrpc: "2.0",
id: null,
error: {
code: MCPErrorCode.INTERNAL_ERROR,
message: "Internal server error",
data: this.debug ? { stack: err.stack } : undefined
}
});
});
}
/**
* Setup Express routes
*/
private setupRoutes(): void {
// Health check endpoint
this.app.get('/health', (req: Request, res: Response) => {
res.status(200).json({
status: 'ok',
transport: 'http',
timestamp: new Date().toISOString()
});
});
// Server info endpoint
this.app.get(`${this.apiPrefix}/info`, (req: Request, res: Response) => {
res.status(200).json({
jsonrpc: "2.0",
result: {
name: "Model Context Protocol Server",
version: "1.0.0",
transport: "http",
protocol: "json-rpc-2.0",
features: ["streaming"],
timestamp: new Date().toISOString()
}
});
});
// SSE stream endpoint
this.app.get(`${this.apiPrefix}/stream`, (req: Request, res: Response) => {
const clientId = (req.query.clientId as string) || `client-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
// Set headers for SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Store the client
this.sseClients.set(clientId, { id: clientId, response: res });
// Send initial connection established event
res.write(`event: connected\ndata: ${JSON.stringify({ clientId })}\n\n`);
// Client disconnection handler
req.on('close', () => {
if (this.debug) {
logger.debug(`SSE client disconnected: ${clientId}`);
}
this.sseClients.delete(clientId);
});
if (this.debug) {
logger.debug(`SSE client connected: ${clientId}`);
}
});
// JSON-RPC endpoint
this.app.post(`${this.apiPrefix}/jsonrpc`, (req: Request, res: Response) => {
void this.handleJsonRpcRequest(req, res);
});
// Default 404 handler
this.app.use((req: Request, res: Response) => {
res.status(404).json({
jsonrpc: "2.0",
id: null,
error: {
code: MCPErrorCode.METHOD_NOT_FOUND,
message: "Not found"
}
});
});
}
/**
* Handle a JSON-RPC request from HTTP
*/
private async handleJsonRpcRequest(req: Request, res: Response): Promise<void> {
if (!this.handler) {
res.status(500).json({
jsonrpc: "2.0",
id: req.body.id || null,
error: {
code: MCPErrorCode.INTERNAL_ERROR,
message: "Transport not properly initialized"
}
});
return;
}
try {
// Validate it's JSON-RPC 2.0
if (!req.body.jsonrpc || req.body.jsonrpc !== "2.0") {
res.status(400).json({
jsonrpc: "2.0",
id: req.body.id || null,
error: {
code: MCPErrorCode.INVALID_REQUEST,
message: "Invalid JSON-RPC 2.0 request: missing or invalid jsonrpc version"
}
});
return;
}
// Check for batch requests
if (Array.isArray(req.body)) {
res.status(501).json({
jsonrpc: "2.0",
id: null,
error: {
code: MCPErrorCode.METHOD_NOT_FOUND,
message: "Batch requests are not supported"
}
});
return;
}
// Handle request
const request: MCPRequest = {
jsonrpc: req.body.jsonrpc,
id: req.body.id ?? null,
method: req.body.method,
params: req.body.params
};
// Get streaming preference from query params
const useStreaming = req.query.stream === 'true';
// Extract client ID if provided (for streaming)
const clientId = (req.query.clientId as string) || (req.body.clientId as string);
// Check if this is a streaming request and client is connected
if (useStreaming && clientId && this.sseClients.has(clientId)) {
// Add streaming metadata to the request
request.streaming = {
enabled: true,
clientId
};
}
// Process the request
const response = await this.handler(request);
// Return the response
res.status(200).json({
jsonrpc: "2.0",
...response
});
} catch (error) {
logger.error(`Error handling JSON-RPC request: ${String(error)}`);
res.status(500).json({
jsonrpc: "2.0",
id: req.body?.id || null,
error: {
code: MCPErrorCode.INTERNAL_ERROR,
message: error instanceof Error ? error.message : "Internal error",
data: this.debug && error instanceof Error ? { stack: error.stack } : undefined
}
});
}
}
/**
* Start the HTTP server
*/
public async start(): Promise<void> {
if (!this.initialized) {
throw new Error("HttpTransport not initialized");
}
return new Promise<void>((resolve, reject) => {
try {
this.server = this.app.listen(this.port, () => {
logger.info(`HTTP transport started on port ${this.port}`);
resolve();
});
// Error handler
this.server.on('error', (err) => {
logger.error(`HTTP server error: ${String(err)}`);
reject(err);
});
} catch (err) {
logger.error(`Failed to start HTTP transport: ${String(err)}`);
reject(err);
}
});
}
/**
* Stop the HTTP server
*/
public async stop(): Promise<void> {
return new Promise<void>((resolve, reject) => {
// Close server if running
if (this.server) {
this.server.close((err) => {
if (err) {
logger.error(`Error shutting down HTTP server: ${String(err)}`);
reject(err);
} else {
logger.info("HTTP transport stopped");
this.server = null;
resolve();
}
});
} else {
resolve();
}
// Close all SSE connections
for (const client of this.sseClients.values()) {
try {
client.response.write(`event: shutdown\ndata: {}\n\n`);
client.response.end();
} catch (err) {
logger.error(`Error closing SSE connection: ${String(err)}`);
}
}
// Clear all clients
this.sseClients.clear();
});
}
/**
* Send an SSE event to a specific client
*/
private sendSSEEvent(clientId: string, event: string, data: unknown): boolean {
const client = this.sseClients.get(clientId);
if (!client) {
return false;
}
try {
const payload = JSON.stringify(data);
client.response.write(`event: ${event}\ndata: ${payload}\n\n`);
return true;
} catch (err) {
logger.error(`Error sending SSE event: ${String(err)}`);
return false;
}
}
/**
* Send a notification to a client
*/
public sendNotification(notification: MCPNotification): void {
// SSE notifications not supported without a client ID
return;
}
/**
* Send a streaming response part
*/
public sendStreamPart(streamPart: MCPStreamPart): void {
// Find the client ID in streaming metadata
const clientId = streamPart.clientId;
if (!clientId || !this.sseClients.has(clientId)) {
logger.warn(`Cannot send stream part: client ${clientId || 'unknown'} not connected`);
return;
}
// Send the stream part as an SSE event
const eventPayload = {
jsonrpc: "2.0",
id: streamPart.id,
stream: {
partId: streamPart.partId,
final: streamPart.final,
data: streamPart.data
}
};
this.sendSSEEvent(clientId, 'stream', eventPayload);
// Debug logging
if (this.debug) {
logger.debug(`Sent stream part to client ${clientId}: partId=${streamPart.partId}, final=${streamPart.final}`);
}
}
/**
* Broadcast a notification to all connected clients
*/
public broadcastNotification(event: string, data: unknown): void {
for (const client of this.sseClients.values()) {
try {
const payload = JSON.stringify(data);
client.response.write(`event: ${event}\ndata: ${payload}\n\n`);
} catch (err) {
logger.error(`Error broadcasting to client ${client.id}: ${String(err)}`);
}
}
}
/**
* Send a log message (not applicable for HTTP transport)
*/
public sendLogMessage(level: string, message: string, data?: unknown): void {
// Log messages in HTTP context go to the logger, not to clients
logger[level as keyof typeof logger]?.(message, data);
}
}

View File

@@ -0,0 +1,329 @@
/**
* Stdio Transport for MCP
*
* This module provides a transport that uses standard input/output
* for JSON-RPC 2.0 communication. This is particularly useful for
* integration with AI assistants like Claude, GPT, and Cursor.
*/
import { BaseTransport } from "../transport.js";
import { logger } from "../../utils/logger.js";
import { MCPServer } from "../MCPServer.js";
import type { MCPRequest, MCPResponse, ToolExecutionResult } from "../types.js";
import { JSONRPCError } from "../utils/error.js";
/**
* StdioTransport options
*/
export interface StdioTransportOptions {
/** Whether to enable silent mode (suppress non-essential output) */
silent?: boolean;
/** Whether to enable debug mode */
debug?: boolean;
/** Reference to an MCPServer instance */
server?: MCPServer;
}
/**
* Transport implementation for standard input/output
* Communicates using JSON-RPC 2.0 protocol
*/
export class StdioTransport extends BaseTransport {
private isStarted = false;
private silent: boolean;
private debug: boolean;
private server: MCPServer | null = null;
constructor(options: StdioTransportOptions = {}) {
super();
this.silent = options.silent ?? false;
this.debug = options.debug ?? false;
if (options.server) {
this.server = options.server;
}
// Configure stdin to not buffer input
process.stdin.setEncoding('utf8');
}
/**
* Set the server reference to access tools and other server properties
*/
public setServer(server: MCPServer): void {
this.server = server;
}
/**
* Start the transport and setup stdin/stdout handlers
*/
public async start(): Promise<void> {
if (this.isStarted) return;
if (!this.silent) {
logger.info('Starting stdio transport');
}
// Setup input handling
this.setupInputHandling();
this.isStarted = true;
if (!this.silent) {
logger.info('Stdio transport started');
}
// Send system info notification
this.sendSystemInfo();
// Send available tools notification
this.sendAvailableTools();
}
/**
* Send system information as a notification
* This helps clients understand the capabilities of the server
*/
private sendSystemInfo(): void {
const notification = {
jsonrpc: '2.0',
method: 'system.info',
params: {
name: 'Home Assistant Model Context Protocol Server',
version: '1.0.0',
transport: 'stdio',
protocol: 'json-rpc-2.0',
features: ['streaming'],
timestamp: new Date().toISOString()
}
};
// Send directly to stdout
process.stdout.write(JSON.stringify(notification) + '\n');
}
/**
* Send available tools as a notification
* This helps clients know what tools are available to use
*/
private sendAvailableTools(): void {
if (!this.server) {
logger.warn('Cannot send available tools: server reference not set');
return;
}
const tools = this.server.getAllTools().map(tool => {
// For parameters, create a simple JSON schema or empty object
const parameters = tool.parameters
? { type: 'object', properties: {} } // Simplified schema
: { type: 'object', properties: {} };
return {
name: tool.name,
description: tool.description,
parameters,
metadata: tool.metadata
};
});
const notification = {
jsonrpc: '2.0',
method: 'tools.available',
params: { tools }
};
// Send directly to stdout
process.stdout.write(JSON.stringify(notification) + '\n');
}
/**
* Set up the input handling for JSON-RPC requests
*/
private setupInputHandling(): void {
let buffer = '';
process.stdin.on('data', (chunk: string) => {
buffer += chunk;
try {
// Look for complete JSON objects by matching opening and closing braces
let startIndex = 0;
let openBraces = 0;
let inString = false;
let escapeNext = false;
for (let i = 0; i < buffer.length; i++) {
const char = buffer[i];
if (escapeNext) {
escapeNext = false;
continue;
}
if (char === '\\' && inString) {
escapeNext = true;
continue;
}
if (char === '"' && !escapeNext) {
inString = !inString;
continue;
}
if (!inString) {
if (char === '{') {
if (openBraces === 0) {
startIndex = i;
}
openBraces++;
} else if (char === '}') {
openBraces--;
if (openBraces === 0) {
// We have a complete JSON object
const jsonStr = buffer.substring(startIndex, i + 1);
this.handleJsonRequest(jsonStr);
// Remove the processed part from the buffer
buffer = buffer.substring(i + 1);
// Reset the parser to start from the beginning of the new buffer
i = -1;
}
}
}
}
} catch (error) {
if (this.debug) {
logger.error('Error processing JSON-RPC input', error);
}
this.sendErrorResponse(null, new JSONRPCError.ParseError('Invalid JSON'));
}
});
process.stdin.on('end', () => {
if (!this.silent) {
logger.info('Stdio transport: stdin ended');
}
process.exit(0);
});
process.stdin.on('error', (error) => {
logger.error('Stdio transport: stdin error', error);
process.exit(1);
});
}
/**
* Handle a JSON-RPC request
*/
private async handleJsonRequest(jsonStr: string): Promise<void> {
try {
const request = JSON.parse(jsonStr);
if (this.debug) {
logger.debug(`Received request: ${jsonStr}`);
}
if (!request.jsonrpc || request.jsonrpc !== '2.0') {
return this.sendErrorResponse(
request.id,
new JSONRPCError.InvalidRequest('Invalid JSON-RPC 2.0 request')
);
}
const mcpRequest: MCPRequest = {
jsonrpc: request.jsonrpc,
id: request.id,
method: request.method,
params: request.params || {}
};
if (!this.server) {
return this.sendErrorResponse(
request.id,
new JSONRPCError.InternalError('Server not available')
);
}
// Delegate to the server to handle the request
if (this.handler) {
const response = await this.handler(mcpRequest);
this.sendResponse(response);
}
} catch (error) {
if (error instanceof SyntaxError) {
this.sendErrorResponse(null, new JSONRPCError.ParseError('Invalid JSON'));
} else {
this.sendErrorResponse(null, new JSONRPCError.InternalError('Internal error'));
}
if (this.debug) {
logger.error('Error handling JSON-RPC request', error);
}
}
}
/**
* Send a JSON-RPC error response
*/
private sendErrorResponse(id: string | number | null, error: JSONRPCError.JSONRPCError): void {
const response = {
jsonrpc: '2.0',
id: id,
error: {
code: error.code,
message: error.message,
data: error.data
}
};
process.stdout.write(JSON.stringify(response) + '\n');
}
/**
* Send an MCPResponse to the client
*/
public sendResponse(response: MCPResponse): void {
const jsonRpcResponse = {
jsonrpc: '2.0',
id: response.id,
...(response.error
? { error: response.error }
: { result: response.result })
};
process.stdout.write(JSON.stringify(jsonRpcResponse) + '\n');
}
/**
* Stream a partial response for long-running operations
*/
public streamResponsePart(requestId: string | number, result: ToolExecutionResult): void {
const streamResponse = {
jsonrpc: '2.0',
method: 'stream.data',
params: {
id: requestId,
data: result
}
};
process.stdout.write(JSON.stringify(streamResponse) + '\n');
}
/**
* Stop the transport
*/
public async stop(): Promise<void> {
if (!this.isStarted) return;
if (!this.silent) {
logger.info('Stopping stdio transport');
}
this.isStarted = false;
}
}

220
src/mcp/types.ts Normal file
View File

@@ -0,0 +1,220 @@
/**
* MCP Type Definitions
*
* This file contains all the type definitions used by the Model Context Protocol
* implementation, including tools, transports, middleware, and resources.
*/
import { z } from "zod";
import { Logger } from "winston";
import { MCPServer, MCPErrorCode, MCPServerEvents } from "./MCPServer.js";
/**
* MCP Server configuration
*/
export interface MCPConfig {
maxRetries: number;
retryDelay: number;
executionTimeout: number;
streamingEnabled: boolean;
maxPayloadSize: number;
}
// Re-export enums from MCPServer
export { MCPErrorCode, MCPServerEvents };
/**
* Tool definition interface
*/
export interface ToolDefinition {
name: string;
description: string;
parameters?: z.ZodType<any>;
returnType?: z.ZodType<any>;
execute: (params: any, context: MCPContext) => Promise<any>;
metadata?: ToolMetadata;
}
/**
* Tool metadata for categorization and discovery
*/
export interface ToolMetadata {
category: string;
version: string;
tags?: string[];
platforms?: string[];
requiresAuth?: boolean;
isStreaming?: boolean;
examples?: ToolExample[];
}
/**
* Example usage for a tool
*/
export interface ToolExample {
description: string;
params: any;
expectedResult?: any;
}
/**
* JSON-RPC Request
*/
export interface MCPRequest {
jsonrpc: string;
id: string | number | null;
method: string;
params?: Record<string, unknown>;
streaming?: {
enabled: boolean;
clientId: string;
};
}
/**
* JSON-RPC 2.0 Response
*/
export interface MCPResponse {
jsonrpc?: string;
id?: string | number;
result?: any;
error?: MCPError;
}
/**
* JSON-RPC 2.0 Error
*/
export interface MCPError {
code: number;
message: string;
data?: any;
}
/**
* JSON-RPC 2.0 Notification
*/
export interface MCPNotification {
jsonrpc?: string;
method: string;
params?: any;
}
/**
* JSON-RPC Stream Part
*/
export interface MCPStreamPart {
id: string | number;
partId: string | number;
final: boolean;
data: unknown;
clientId?: string;
}
/**
* Response Stream Interface for streaming operation results
*/
export interface MCPResponseStream {
/**
* Write partial result data to the stream
*
* @param data The partial result data
* @returns True if the write was successful, false otherwise
*/
write(data: any): boolean;
/**
* End the stream, indicating no more data will be sent
*
* @param data Optional final data to send
*/
end(data?: any): void;
/**
* Check if streaming is enabled
*/
readonly isEnabled: boolean;
/**
* Get the client ID for this stream
*/
readonly clientId?: string;
}
/**
* Context for tool execution
*/
export interface MCPContext {
requestId: string | number;
startTime: number;
resourceManager: ResourceManager;
tools: Map<string, ToolDefinition>;
config: MCPConfig;
logger: Logger;
server: MCPServer;
state?: Map<string, any>;
}
/**
* Resource manager interface
*/
export interface ResourceManager {
acquire: (resourceType: string, resourceId: string, context: MCPContext) => Promise<any>;
release: (resourceType: string, resourceId: string, context: MCPContext) => Promise<void>;
list: (context: MCPContext, resourceType?: string) => Promise<string[]>;
}
/**
* Middleware function type
*/
export type MCPMiddleware = (
request: MCPRequest,
context: MCPContext,
next: () => Promise<MCPResponse>
) => Promise<MCPResponse>;
/**
* Transport layer interface
*/
export interface TransportLayer {
name: string;
initialize: (handler: (request: MCPRequest) => Promise<MCPResponse>) => void;
start: () => Promise<void>;
stop: () => Promise<void>;
sendNotification?: (notification: MCPNotification) => void;
sendStreamPart?: (streamPart: MCPStreamPart) => void;
}
/**
* Claude-specific function call formats
*/
export interface ClaudeFunctionDefinition {
name: string;
description: string;
parameters: {
type: string;
properties: Record<string, {
type: string;
description: string;
enum?: string[];
}>;
required: string[];
};
}
/**
* Cursor-specific integration types
*/
export interface CursorToolDefinition {
name: string;
description: string;
parameters: Record<string, {
type: string;
description: string;
required: boolean;
}>;
}
/**
* Tool execution result type used in streaming responses
*/
export type ToolExecutionResult = any;

129
src/mcp/utils/claude.ts Normal file
View File

@@ -0,0 +1,129 @@
/**
* Claude Integration Utilities
*
* This file contains utilities for integrating with Claude AI models.
*/
import { z } from 'zod';
import { ToolDefinition } from '../types.js';
/**
* Convert a Zod schema to a JSON Schema for Claude
*/
export function zodToJsonSchema(schema: z.ZodType<any>): any {
if (!schema) return { type: 'object', properties: {} };
// Handle ZodObject
if (schema instanceof z.ZodObject) {
const shape = (schema as any)._def.shape();
const properties: Record<string, any> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(shape)) {
if (!(value instanceof z.ZodOptional)) {
required.push(key);
}
properties[key] = zodTypeToJsonSchema(value as z.ZodType<any>);
}
return {
type: 'object',
properties,
required: required.length > 0 ? required : undefined
};
}
// Handle other schema types
return { type: 'object', properties: {} };
}
/**
* Convert a Zod type to JSON Schema type
*/
export function zodTypeToJsonSchema(zodType: z.ZodType<any>): any {
if (zodType instanceof z.ZodString) {
return { type: 'string' };
} else if (zodType instanceof z.ZodNumber) {
return { type: 'number' };
} else if (zodType instanceof z.ZodBoolean) {
return { type: 'boolean' };
} else if (zodType instanceof z.ZodArray) {
return {
type: 'array',
items: zodTypeToJsonSchema((zodType as any)._def.type)
};
} else if (zodType instanceof z.ZodEnum) {
return {
type: 'string',
enum: (zodType as any)._def.values
};
} else if (zodType instanceof z.ZodOptional) {
return zodTypeToJsonSchema((zodType as any)._def.innerType);
} else if (zodType instanceof z.ZodObject) {
return zodToJsonSchema(zodType);
}
return { type: 'object' };
}
/**
* Create Claude-compatible tool definitions from MCP tools
*
* @param tools Array of MCP tool definitions
* @returns Array of Claude-compatible tool definitions
*/
export function createClaudeToolDefinitions(tools: ToolDefinition[]): any[] {
return tools.map(tool => {
const parameters = tool.parameters
? zodToJsonSchema(tool.parameters)
: { type: 'object', properties: {} };
return {
name: tool.name,
description: tool.description,
parameters
};
});
}
/**
* Format an MCP tool execution request for Claude
*/
export function formatToolExecutionRequest(toolName: string, params: Record<string, unknown>): any {
return {
type: 'tool_use',
name: toolName,
parameters: params
};
}
/**
* Parse a Claude tool execution response
*/
export function parseToolExecutionResponse(response: any): {
success: boolean;
result?: any;
error?: string;
} {
if (!response || typeof response !== 'object') {
return {
success: false,
error: 'Invalid tool execution response'
};
}
if ('error' in response) {
return {
success: false,
error: typeof response.error === 'string'
? response.error
: JSON.stringify(response.error)
};
}
return {
success: true,
result: response
};
}

131
src/mcp/utils/cursor.ts Normal file
View File

@@ -0,0 +1,131 @@
/**
* Cursor Integration Utilities
*
* This file contains utilities for integrating with Cursor IDE.
*/
import { z } from 'zod';
import { ToolDefinition } from '../types.js';
/**
* Create Cursor-compatible tool definitions from MCP tools
*
* @param tools Array of MCP tool definitions
* @returns Array of Cursor-compatible tool definitions
*/
export function createCursorToolDefinitions(tools: ToolDefinition[]): any[] {
return tools.map(tool => {
// Convert parameters to Cursor format
const parameters = tool.parameters
? extractParametersFromZod(tool.parameters)
: {};
return {
name: tool.name,
description: tool.description,
parameters
};
});
}
/**
* Extract parameters from a Zod schema for Cursor integration
*/
function extractParametersFromZod(schema: z.ZodType<any>): Record<string, any> {
if (!(schema instanceof z.ZodObject)) {
return {};
}
const shape = (schema as any)._def.shape();
const params: Record<string, any> = {};
for (const [key, value] of Object.entries(shape)) {
const isRequired = !(value instanceof z.ZodOptional);
let type = 'string';
let description = '';
// Get description if available
try {
description = value._def.description || '';
} catch (e) {
// Ignore if description is not available
}
// Determine the type
if (value instanceof z.ZodString) {
type = 'string';
} else if (value instanceof z.ZodNumber) {
type = 'number';
} else if (value instanceof z.ZodBoolean) {
type = 'boolean';
} else if (value instanceof z.ZodArray) {
type = 'array';
} else if (value instanceof z.ZodEnum) {
type = 'string';
} else if (value instanceof z.ZodObject) {
type = 'object';
} else if (value instanceof z.ZodOptional) {
// Get the inner type
const innerValue = value._def.innerType;
if (innerValue instanceof z.ZodString) {
type = 'string';
} else if (innerValue instanceof z.ZodNumber) {
type = 'number';
} else if (innerValue instanceof z.ZodBoolean) {
type = 'boolean';
} else if (innerValue instanceof z.ZodArray) {
type = 'array';
} else {
type = 'object';
}
}
params[key] = {
type,
description,
required: isRequired
};
}
return params;
}
/**
* Format a tool response for Cursor
*/
export function formatCursorResponse(response: any): any {
// For now, just return the response as-is
// Cursor expects a specific format, which may need to be customized
return response;
}
/**
* Parse a Cursor tool execution request
*/
export function parseCursorRequest(request: any): {
success: boolean;
toolName?: string;
params?: Record<string, any>;
error?: string;
} {
if (!request || typeof request !== 'object') {
return {
success: false,
error: 'Invalid request format'
};
}
if (!request.name || typeof request.name !== 'string') {
return {
success: false,
error: 'Missing or invalid tool name'
};
}
return {
success: true,
toolName: request.name,
params: request.parameters || {}
};
}

194
src/mcp/utils/error.ts Normal file
View File

@@ -0,0 +1,194 @@
/**
* Error Handling Utilities
*
* This file contains utilities for handling errors in the MCP implementation.
*/
import { MCPErrorCode, MCPError } from '../types.js';
/**
* Create an MCP error object
*/
export function createError(
code: MCPErrorCode,
message: string,
data?: unknown
): MCPError {
return {
code,
message,
data
};
}
/**
* Format an error for JSON-RPC response
*/
export function formatJsonRpcError(
id: string | number | null,
code: MCPErrorCode,
message: string,
data?: unknown
): any {
return {
jsonrpc: '2.0',
id,
error: {
code,
message,
data
}
};
}
/**
* Handle unexpected errors and convert to MCPError
*/
export function handleUnexpectedError(error: unknown): MCPError {
if (error instanceof Error) {
return {
code: MCPErrorCode.INTERNAL_ERROR,
message: error.message,
data: {
name: error.name,
stack: error.stack
}
};
}
return {
code: MCPErrorCode.INTERNAL_ERROR,
message: 'An unexpected error occurred',
data: error
};
}
/**
* Safe JSON stringify with circular reference handling
*/
export function safeStringify(obj: unknown): string {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
}
return value;
});
}
/**
* JSON-RPC error related utilities and classes
*/
export namespace JSONRPCError {
/**
* Standard JSON-RPC 2.0 error codes
*/
export enum ErrorCode {
PARSE_ERROR = -32700,
INVALID_REQUEST = -32600,
METHOD_NOT_FOUND = -32601,
INVALID_PARAMS = -32602,
INTERNAL_ERROR = -32603,
// Implementation specific error codes
SERVER_ERROR_START = -32099,
SERVER_ERROR_END = -32000,
// MCP specific error codes
TOOL_EXECUTION_ERROR = -32000,
VALIDATION_ERROR = -32001,
}
/**
* Base JSON-RPC Error class
*/
export class JSONRPCError extends Error {
public code: number;
public data?: unknown;
constructor(message: string, code: number, data?: unknown) {
super(message);
this.name = 'JSONRPCError';
this.code = code;
this.data = data;
}
}
/**
* Parse Error (-32700)
* Invalid JSON was received by the server.
*/
export class ParseError extends JSONRPCError {
constructor(message: string = 'Parse error', data?: unknown) {
super(message, ErrorCode.PARSE_ERROR, data);
this.name = 'ParseError';
}
}
/**
* Invalid Request (-32600)
* The JSON sent is not a valid Request object.
*/
export class InvalidRequest extends JSONRPCError {
constructor(message: string = 'Invalid request', data?: unknown) {
super(message, ErrorCode.INVALID_REQUEST, data);
this.name = 'InvalidRequest';
}
}
/**
* Method Not Found (-32601)
* The method does not exist / is not available.
*/
export class MethodNotFound extends JSONRPCError {
constructor(message: string = 'Method not found', data?: unknown) {
super(message, ErrorCode.METHOD_NOT_FOUND, data);
this.name = 'MethodNotFound';
}
}
/**
* Invalid Params (-32602)
* Invalid method parameter(s).
*/
export class InvalidParams extends JSONRPCError {
constructor(message: string = 'Invalid params', data?: unknown) {
super(message, ErrorCode.INVALID_PARAMS, data);
this.name = 'InvalidParams';
}
}
/**
* Internal Error (-32603)
* Internal JSON-RPC error.
*/
export class InternalError extends JSONRPCError {
constructor(message: string = 'Internal error', data?: unknown) {
super(message, ErrorCode.INTERNAL_ERROR, data);
this.name = 'InternalError';
}
}
/**
* Tool Execution Error (-32000)
* Error during tool execution.
*/
export class ToolExecutionError extends JSONRPCError {
constructor(message: string = 'Tool execution error', data?: unknown) {
super(message, ErrorCode.TOOL_EXECUTION_ERROR, data);
this.name = 'ToolExecutionError';
}
}
/**
* Validation Error (-32001)
* Error during validation of params or result.
*/
export class ValidationError extends JSONRPCError {
constructor(message: string = 'Validation error', data?: unknown) {
super(message, ErrorCode.VALIDATION_ERROR, data);
this.name = 'ValidationError';
}
}
}

147
src/stdio-server.ts Normal file
View File

@@ -0,0 +1,147 @@
/**
* MCP Server with stdio transport
*
* This module provides a standalone MCP server that communicates
* over standard input/output using JSON-RPC 2.0 protocol.
*/
// Force silent logging
process.env.LOG_LEVEL = 'silent';
import { createStdioServer, BaseTool } from "./mcp/index.js";
import { z } from "zod";
import { logger } from "./utils/logger.js";
import { MCPContext } from "./mcp/types.js";
// Import Home Assistant tools
import { LightsControlTool } from './tools/homeassistant/lights.tool.js';
import { ClimateControlTool } from './tools/homeassistant/climate.tool.js';
// Check for silent startup mode - never silent in npx mode to ensure the JSON-RPC messages are sent
const silentStartup = true;
const debugMode = process.env.DEBUG_STDIO === 'true';
// Send a notification directly to stdout for Cursor compatibility
function sendNotification(method: string, params: any): void {
const notification = {
jsonrpc: '2.0',
method,
params
};
process.stdout.write(JSON.stringify(notification) + '\n');
}
// Create system tools
class InfoTool extends BaseTool {
constructor() {
super({
name: "system_info",
description: "Get information about the Home Assistant MCP server",
parameters: z.object({}).optional(),
metadata: {
category: "system",
version: "1.0.0",
tags: ["system", "info"]
}
});
}
execute(_params: any, _context: MCPContext): any {
return {
version: "1.0.0",
name: "Home Assistant MCP Server",
mode: "stdio",
transport: "json-rpc-2.0",
features: ["streaming", "middleware", "validation"],
timestamp: new Date().toISOString(),
homeAssistant: {
available: true,
toolCount: 2,
toolNames: ["lights_control", "climate_control"]
}
};
}
}
async function main() {
try {
// Create system tools
const systemTools = [
new InfoTool()
];
// Create Home Assistant tools
const haTools = [
new LightsControlTool(),
new ClimateControlTool()
];
// Combine all tools
const allTools = [...systemTools, ...haTools];
// Create server with stdio transport
const { server, transport } = createStdioServer({
silent: silentStartup,
debug: debugMode,
tools: allTools
});
// Explicitly set the server reference to ensure access to tools
if ('setServer' in transport && typeof transport.setServer === 'function') {
transport.setServer(server);
}
// Send initial notifications directly to stdout for Cursor compatibility
// Send system info
sendNotification('system.info', {
name: 'Home Assistant Model Context Protocol Server',
version: '1.0.0',
transport: 'stdio',
protocol: 'json-rpc-2.0',
features: ['streaming'],
timestamp: new Date().toISOString()
});
// Send available tools
const toolDefinitions = allTools.map(tool => ({
name: tool.name,
description: tool.description,
parameters: {
type: "object",
properties: {},
required: []
},
metadata: tool.metadata
}));
sendNotification('tools.available', {
tools: toolDefinitions
});
// Start the server
await server.start();
// Handle process exit
process.on('SIGINT', async () => {
await server.shutdown();
process.exit(0);
});
process.on('SIGTERM', async () => {
await server.shutdown();
process.exit(0);
});
// Keep process alive
process.stdin.resume();
} catch (error) {
logger.error("Error starting Home Assistant MCP stdio server:", error);
process.exit(1);
}
}
// Run the main function
main().catch(error => {
logger.error("Uncaught error:", error);
process.exit(1);
});

242
src/tools/base-tool.ts Normal file
View File

@@ -0,0 +1,242 @@
/**
* Base Tool Class
*
* This abstract class provides common functionality for all tools,
* including parameter validation, execution context, error handling,
* and support for streaming responses.
*/
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import {
ToolDefinition,
ToolMetadata,
MCPContext,
MCPStreamPart,
MCPErrorCode
} from "../mcp/types.js";
/**
* Abstract base class for all tools
*/
export abstract class BaseTool implements ToolDefinition {
public name: string;
public description: string;
public parameters?: z.ZodType<any>;
public returnType?: z.ZodType<any>;
public metadata?: ToolMetadata;
/**
* Constructor
*/
constructor(props: {
name: string;
description: string;
parameters?: z.ZodType<any>;
returnType?: z.ZodType<any>;
metadata?: Partial<ToolMetadata>;
}) {
this.name = props.name;
this.description = props.description;
this.parameters = props.parameters;
this.returnType = props.returnType;
// Set default metadata
this.metadata = {
category: "general",
version: "1.0.0",
...props.metadata
};
}
/**
* Main execute method to be implemented by subclasses
*/
public abstract execute(params: any, context: MCPContext): Promise<any>;
/**
* Validate parameters against schema
*/
protected validateParams(params: any): any {
if (!this.parameters) {
return params;
}
try {
return this.parameters.parse(params);
} catch (error) {
throw {
code: MCPErrorCode.VALIDATION_ERROR,
message: `Invalid parameters for tool '${this.name}'`,
data: error
};
}
}
/**
* Validate result against schema
*/
protected validateResult(result: any): any {
if (!this.returnType) {
return result;
}
try {
return this.returnType.parse(result);
} catch (error) {
throw {
code: MCPErrorCode.VALIDATION_ERROR,
message: `Invalid result from tool '${this.name}'`,
data: error
};
}
}
/**
* Send a streaming response part
*/
protected sendStreamPart(data: any, context: MCPContext, isFinal: boolean = false): void {
// Get requestId from context
const { requestId, server } = context;
// Get active transports with streaming support
const streamingTransports = Array.from(server["transports"])
.filter(transport => !!transport.sendStreamPart);
if (streamingTransports.length === 0) {
context.logger.warn(
`Tool '${this.name}' attempted to stream, but no transports support streaming`
);
return;
}
// Create stream part message
const streamPart: MCPStreamPart = {
id: requestId,
partId: uuidv4(),
final: isFinal,
data: data
};
// Send to all transports with streaming support
for (const transport of streamingTransports) {
transport.sendStreamPart(streamPart);
}
}
/**
* Create a streaming executor wrapper
*/
protected createStreamingExecutor<T>(
generator: (params: any, context: MCPContext) => AsyncGenerator<T, T, void>,
context: MCPContext
): (params: any) => Promise<T> {
return async (params: any): Promise<T> => {
const validParams = this.validateParams(params);
let finalResult: T | undefined = undefined;
try {
const gen = generator(validParams, context);
for await (const chunk of gen) {
// Send intermediate result
this.sendStreamPart(chunk, context, false);
finalResult = chunk;
}
if (finalResult !== undefined) {
// Validate and send final result
const validResult = this.validateResult(finalResult);
this.sendStreamPart(validResult, context, true);
return validResult;
}
throw new Error("Streaming generator did not produce a final result");
} catch (error) {
context.logger.error(`Error in streaming tool '${this.name}':`, error);
throw error;
}
};
}
/**
* Convert tool to SchemaObject format (for Claude and OpenAI)
*/
public toSchemaObject(): any {
// Convert Zod schema to JSON Schema for parameters
const parametersSchema = this.parameters ? this.zodToJsonSchema(this.parameters) : {
type: "object",
properties: {},
required: []
};
return {
name: this.name,
description: this.description,
parameters: parametersSchema
};
}
/**
* Convert Zod schema to JSON Schema (simplified)
*/
private zodToJsonSchema(schema: z.ZodType<any>): any {
// This is a simplified conversion - in production you'd want a full implementation
// or use a library like zod-to-json-schema
// Basic implementation just to support our needs
if (schema instanceof z.ZodObject) {
const shape = (schema as any)._def.shape();
const properties: Record<string, any> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(shape)) {
// Add to required array if the field is required
if (!(value instanceof z.ZodOptional)) {
required.push(key);
}
// Convert property - explicitly cast value to ZodType to fix linter error
properties[key] = this.zodTypeToJsonType(value as z.ZodType<any>);
}
return {
type: "object",
properties,
required: required.length > 0 ? required : undefined
};
}
// Fallback for other schema types
return { type: "object" };
}
/**
* Convert Zod type to JSON Schema type (simplified)
*/
private zodTypeToJsonType(zodType: z.ZodType<any>): any {
if (zodType instanceof z.ZodString) {
return { type: "string" };
} else if (zodType instanceof z.ZodNumber) {
return { type: "number" };
} else if (zodType instanceof z.ZodBoolean) {
return { type: "boolean" };
} else if (zodType instanceof z.ZodArray) {
return {
type: "array",
items: this.zodTypeToJsonType((zodType as any)._def.type)
};
} else if (zodType instanceof z.ZodEnum) {
return {
type: "string",
enum: (zodType as any)._def.values
};
} else if (zodType instanceof z.ZodOptional) {
return this.zodTypeToJsonType((zodType as any)._def.innerType);
} else if (zodType instanceof z.ZodObject) {
return this.zodToJsonSchema(zodType);
}
return { type: "object" };
}
}

168
src/tools/example.tool.ts Normal file
View File

@@ -0,0 +1,168 @@
/**
* Example Tool Implementation
*
* This file demonstrates how to create tools using the new BaseTool class,
* including streaming responses and parameter validation.
*/
import { z } from "zod";
import { BaseTool } from "../mcp/index.js";
import { MCPContext } from "../mcp/types.js";
/**
* Example streaming tool that generates a series of responses
*/
export class StreamGeneratorTool extends BaseTool {
constructor() {
super({
name: "stream_generator",
description: "Generate a stream of data with configurable delay and count",
parameters: z.object({
count: z.number().int().min(1).max(20).default(5)
.describe("Number of items to generate (1-20)"),
delay: z.number().int().min(100).max(2000).default(500)
.describe("Delay in ms between items (100-2000)"),
prefix: z.string().optional().default("Item")
.describe("Optional prefix for item labels")
}),
metadata: {
category: "examples",
version: "1.0.0",
tags: ["streaming", "demo"],
isStreaming: true
}
});
}
/**
* Execute method that demonstrates streaming capabilities
*/
async execute(params: {
count: number;
delay: number;
prefix: string;
}, context: MCPContext): Promise<any> {
// Create streaming executor from generator function
const streamingExecutor = this.createStreamingExecutor(
this.generateItems.bind(this),
context
);
// Execute with validated parameters
return streamingExecutor(params);
}
/**
* Generator function that produces stream parts
*/
private async *generateItems(params: {
count: number;
delay: number;
prefix: string;
}, context: MCPContext): AsyncGenerator<any, any, void> {
const { count, delay, prefix } = params;
const results = [];
// Helper function to create a delay
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
// Generate items with delay
for (let i = 1; i <= count; i++) {
// Sleep to simulate async work
await sleep(delay);
// Create an item
const item = {
id: i,
label: `${prefix} ${i}`,
timestamp: new Date().toISOString(),
progress: Math.round((i / count) * 100)
};
results.push(item);
// Yield current results for streaming
yield {
items: [...results],
completed: i,
total: count,
progress: Math.round((i / count) * 100)
};
}
// Final result - this will also be returned from the execute method
return {
items: results,
completed: count,
total: count,
progress: 100,
finished: true
};
}
}
/**
* Example tool that validates complex input
*/
export class ValidationDemoTool extends BaseTool {
constructor() {
super({
name: "validation_demo",
description: "Demonstrates parameter validation with Zod schemas",
parameters: z.object({
user: z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().min(13).optional()
}).describe("User information"),
preferences: z.object({
theme: z.enum(["light", "dark", "system"]).default("system"),
notifications: z.boolean().default(true)
}).optional().describe("User preferences"),
tags: z.array(z.string()).min(1).max(5).optional()
.describe("Optional list of tags (1-5)")
}),
metadata: {
category: "examples",
version: "1.0.0",
tags: ["validation", "demo"]
}
});
}
/**
* Execute method that demonstrates parameter validation
*/
async execute(params: {
user: {
name: string;
email: string;
age?: number;
},
preferences?: {
theme: "light" | "dark" | "system";
notifications: boolean;
},
tags?: string[];
}, context: MCPContext): Promise<any> {
// We don't need to validate here since the BaseTool does it for us
// This just demonstrates how validated parameters look
// Access validated and defaulted parameters
const { user, preferences, tags } = params;
// Wait to simulate async processing
await new Promise(resolve => setTimeout(resolve, 500));
// Return validated data with additional information
return {
validated: true,
timestamp: new Date().toISOString(),
requestId: context.requestId,
user,
preferences: preferences || { theme: "system", notifications: true },
tags: tags || [],
message: `Hello ${user.name}, your validation was successful!`
};
}
}

View File

@@ -0,0 +1,115 @@
/**
* Example Tool: Stream Generator
*
* This tool demonstrates how to implement streaming functionality in MCP tools.
* It generates a stream of data that can be consumed by clients in real-time.
*/
import { z } from 'zod';
import { BaseTool } from '../../mcp/BaseTool.js';
import { MCPResponseStream } from '../../mcp/types.js';
// Schema for the stream generator parameters
const streamGeneratorSchema = z.object({
count: z.number().int().min(1).max(100).default(10)
.describe('Number of items to generate in the stream (1-100)'),
delay: z.number().int().min(100).max(2000).default(500)
.describe('Delay between items in milliseconds (100-2000)'),
includeTimestamp: z.boolean().default(false)
.describe('Whether to include timestamp with each streamed item'),
failAfter: z.number().int().min(0).default(0)
.describe('If greater than 0, fail after this many items (for error handling testing)')
});
// Define the parameter and result types
type StreamGeneratorParams = z.infer<typeof streamGeneratorSchema>;
type StreamGeneratorResult = {
message: string;
count: number;
timestamp?: string;
items: string[];
};
/**
* A tool that demonstrates streaming capabilities by generating a stream of data
* with configurable parameters for count, delay, and error scenarios.
*/
export class StreamGeneratorTool extends BaseTool<StreamGeneratorParams, StreamGeneratorResult> {
constructor() {
super({
name: 'stream_generator',
description: 'Generates a stream of data with configurable delay and count',
version: '1.0.0',
parameters: streamGeneratorSchema,
});
}
/**
* Execute the tool and stream results back to the client
*/
async execute(
params: StreamGeneratorParams,
stream?: MCPResponseStream
): Promise<StreamGeneratorResult> {
const { count, delay, includeTimestamp, failAfter } = params;
const items: string[] = [];
// If we have a stream, use it to send intermediate results
if (stream) {
for (let i = 1; i <= count; i++) {
// Simulate a processing delay
await new Promise(resolve => setTimeout(resolve, delay));
// Check if we should fail for testing error handling
if (failAfter > 0 && i > failAfter) {
throw new Error(`Intentional failure after ${failAfter} items (for testing)`);
}
const item = `Item ${i} of ${count}`;
items.push(item);
// Create the intermediate result
const partialResult: Partial<StreamGeneratorResult> = {
message: `Generated ${i} of ${count} items`,
count: i,
items: [...items]
};
// Add timestamp if requested
if (includeTimestamp) {
partialResult.timestamp = new Date().toISOString();
}
// Stream the intermediate result
stream.write(partialResult);
}
} else {
// No streaming, generate all items at once with delay between
for (let i = 1; i <= count; i++) {
await new Promise(resolve => setTimeout(resolve, delay));
if (failAfter > 0 && i > failAfter) {
throw new Error(`Intentional failure after ${failAfter} items (for testing)`);
}
items.push(`Item ${i} of ${count}`);
}
}
// Return the final result
const result: StreamGeneratorResult = {
message: `Successfully generated ${count} items`,
count,
items
};
if (includeTimestamp) {
result.timestamp = new Date().toISOString();
}
return result;
}
}

View File

@@ -0,0 +1,92 @@
/**
* Example Tool: Validation Demo
*
* This tool demonstrates how to implement validation using Zod schemas
* in MCP tools. It provides examples of different validation rules and
* how they can be applied to tool parameters.
*/
import { z } from 'zod';
import { BaseTool } from '../../mcp/BaseTool.js';
// Define a complex schema with various validation rules
const validationDemoSchema = z.object({
// String validations
email: z.string().email()
.describe('An email address to validate'),
url: z.string().url().optional()
.describe('Optional URL to validate'),
// Number validations
age: z.number().int().min(18).max(120)
.describe('Age (must be between 18-120)'),
score: z.number().min(0).max(100).default(50)
.describe('Score from 0-100'),
// Array validations
tags: z.array(z.string().min(2).max(20))
.min(1).max(5)
.describe('Between 1-5 tags, each 2-20 characters'),
// Enum validations
role: z.enum(['admin', 'user', 'guest'])
.describe('User role (admin, user, or guest)'),
// Object validations
preferences: z.object({
theme: z.enum(['light', 'dark', 'system']).default('system')
.describe('UI theme preference'),
notifications: z.boolean().default(true)
.describe('Whether to enable notifications'),
language: z.string().default('en')
.describe('Preferred language code')
}).optional()
.describe('Optional user preferences')
});
// Define types based on the schema
type ValidationDemoParams = z.infer<typeof validationDemoSchema>;
type ValidationDemoResult = {
valid: boolean;
message: string;
validatedData: ValidationDemoParams;
metadata: {
fieldsValidated: string[];
timestamp: string;
};
};
/**
* A tool that demonstrates parameter validation using Zod schemas
*/
export class ValidationDemoTool extends BaseTool<ValidationDemoParams, ValidationDemoResult> {
constructor() {
super({
name: 'validation_demo',
description: 'Demonstrates parameter validation using Zod schemas',
version: '1.0.0',
parameters: validationDemoSchema,
});
}
/**
* Execute the validation demo tool
*/
async execute(params: ValidationDemoParams): Promise<ValidationDemoResult> {
// Get all field names that were validated
const fieldsValidated = Object.keys(params);
// Process the validated data (in a real tool, this would do something useful)
return {
valid: true,
message: 'All parameters successfully validated',
validatedData: params,
metadata: {
fieldsValidated,
timestamp: new Date().toISOString()
}
};
}
}

View File

@@ -0,0 +1,403 @@
/**
* Climate Control Tool for Home Assistant
*
* This tool allows controlling climate devices (thermostats, AC units, etc.)
* in Home Assistant through the MCP. It supports modes, temperature settings,
* and fan modes.
*/
import { z } from "zod";
import { BaseTool } from "../base-tool.js";
import { logger } from "../../utils/logger.js";
import { MCPContext } from "../../mcp/types.js";
// Mock Home Assistant API service in absence of actual HA integration
class MockHAClimateService {
private climateDevices: Map<string, {
state: "on" | "off";
hvac_mode: "off" | "heat" | "cool" | "auto" | "dry" | "fan_only";
temperature?: number;
target_temp_high?: number;
target_temp_low?: number;
fan_mode?: "auto" | "low" | "medium" | "high";
friendly_name: string;
supported_features: string[];
current_temperature?: number;
humidity?: number;
}>;
constructor() {
// Initialize with some mock climate devices
this.climateDevices = new Map([
["climate.living_room", {
state: "on",
hvac_mode: "cool",
temperature: 72,
fan_mode: "auto",
friendly_name: "Living Room Thermostat",
supported_features: ["target_temperature", "fan_mode"],
current_temperature: 75
}],
["climate.bedroom", {
state: "off",
hvac_mode: "off",
temperature: 68,
fan_mode: "low",
friendly_name: "Bedroom Thermostat",
supported_features: ["target_temperature", "fan_mode"],
current_temperature: 70
}],
["climate.kitchen", {
state: "on",
hvac_mode: "heat",
temperature: 70,
fan_mode: "medium",
friendly_name: "Kitchen Thermostat",
supported_features: ["target_temperature", "fan_mode"],
current_temperature: 68,
humidity: 45
}],
["climate.office", {
state: "on",
hvac_mode: "auto",
target_temp_high: 78,
target_temp_low: 70,
fan_mode: "auto",
friendly_name: "Office Thermostat",
supported_features: ["target_temperature_range", "fan_mode"],
current_temperature: 72,
humidity: 40
}]
]);
}
// Get all climate devices
public getClimateDevices(): Record<string, unknown>[] {
const result = [];
for (const [entity_id, device] of this.climateDevices.entries()) {
result.push({
entity_id,
state: device.state,
attributes: {
...device,
friendly_name: device.friendly_name
}
});
}
return result;
}
// Get a specific climate device
public getClimateDevice(entity_id: string): Record<string, unknown> | null {
const device = this.climateDevices.get(entity_id);
if (!device) {
return null;
}
return {
entity_id,
state: device.state,
attributes: {
...device,
friendly_name: device.friendly_name
}
};
}
// Set HVAC mode
public setHVACMode(entity_id: string, hvac_mode: string): boolean {
const device = this.climateDevices.get(entity_id);
if (!device) {
return false;
}
// Validate mode
if (!["off", "heat", "cool", "auto", "dry", "fan_only"].includes(hvac_mode)) {
return false;
}
// Set mode
device.hvac_mode = hvac_mode as any;
// Update state based on mode
device.state = hvac_mode === "off" ? "off" : "on";
this.climateDevices.set(entity_id, device);
return true;
}
// Set temperature
public setTemperature(
entity_id: string,
temperature?: number,
target_temp_high?: number,
target_temp_low?: number
): boolean {
const device = this.climateDevices.get(entity_id);
if (!device) {
return false;
}
// Single temperature setting
if (temperature !== undefined &&
device.supported_features.includes("target_temperature")) {
device.temperature = temperature;
}
// Temperature range setting
if (target_temp_high !== undefined &&
target_temp_low !== undefined &&
device.supported_features.includes("target_temperature_range")) {
device.target_temp_high = target_temp_high;
device.target_temp_low = target_temp_low;
}
this.climateDevices.set(entity_id, device);
return true;
}
// Set fan mode
public setFanMode(entity_id: string, fan_mode: string): boolean {
const device = this.climateDevices.get(entity_id);
if (!device) {
return false;
}
// Validate fan mode
if (!["auto", "low", "medium", "high"].includes(fan_mode)) {
return false;
}
// Check if fan mode is supported
if (!device.supported_features.includes("fan_mode")) {
return false;
}
// Set fan mode
device.fan_mode = fan_mode as any;
this.climateDevices.set(entity_id, device);
return true;
}
}
// Singleton instance
const haClimateService = new MockHAClimateService();
// Define the schema for our tool parameters
const climateControlSchema = z.object({
action: z.enum(["list", "get", "set_hvac_mode", "set_temperature", "set_fan_mode"]).describe("The action to perform"),
entity_id: z.string().optional().describe("The entity ID of the climate device to control"),
hvac_mode: z.enum(["off", "heat", "cool", "auto", "dry", "fan_only"]).optional().describe("The HVAC mode to set"),
temperature: z.number().optional().describe("The target temperature to set"),
target_temp_high: z.number().optional().describe("The maximum target temperature to set"),
target_temp_low: z.number().optional().describe("The minimum target temperature to set"),
fan_mode: z.enum(["auto", "low", "medium", "high"]).optional().describe("The fan mode to set"),
});
type ClimateControlParams = z.infer<typeof climateControlSchema>;
/**
* Tool for controlling climate devices in Home Assistant
*/
export class ClimateControlTool extends BaseTool {
constructor() {
super({
name: "climate_control",
description: "Control climate devices in Home Assistant",
parameters: climateControlSchema,
metadata: {
category: "home_assistant",
version: "1.0.0",
tags: ["climate", "thermostat", "hvac", "home_assistant"],
examples: [
{
description: "List all climate devices",
params: { action: "list" }
},
{
description: "Set temperature",
params: {
action: "set_temperature",
entity_id: "climate.living_room",
temperature: 72
}
}
]
}
});
}
/**
* Execute the tool
*/
public async execute(params: ClimateControlParams, context: MCPContext): Promise<Record<string, unknown>> {
logger.debug(`Executing ClimateControlTool with params: ${JSON.stringify(params)}`);
try {
// Add an await here to satisfy the linter
await Promise.resolve();
switch (params.action) {
case "list":
return this.listClimateDevices();
case "get":
if (!params.entity_id) {
throw new Error("entity_id is required for get action");
}
return this.getClimateDevice(params.entity_id);
case "set_hvac_mode":
if (!params.entity_id) {
throw new Error("entity_id is required for set_hvac_mode action");
}
if (!params.hvac_mode) {
throw new Error("hvac_mode is required for set_hvac_mode action");
}
return this.setHVACMode(params.entity_id, params.hvac_mode);
case "set_temperature":
if (!params.entity_id) {
throw new Error("entity_id is required for set_temperature action");
}
if (params.temperature === undefined &&
(params.target_temp_high === undefined || params.target_temp_low === undefined)) {
throw new Error("Either temperature or both target_temp_high and target_temp_low are required");
}
return this.setTemperature(
params.entity_id,
params.temperature,
params.target_temp_high,
params.target_temp_low
);
case "set_fan_mode":
if (!params.entity_id) {
throw new Error("entity_id is required for set_fan_mode action");
}
if (!params.fan_mode) {
throw new Error("fan_mode is required for set_fan_mode action");
}
return this.setFanMode(params.entity_id, params.fan_mode);
default:
throw new Error(`Unknown action: ${String(params.action)}`);
}
} catch (error) {
logger.error(`Error in ClimateControlTool: ${String(error)}`);
throw error;
}
}
/**
* List all climate devices
*/
private listClimateDevices(): Record<string, unknown> {
const devices = haClimateService.getClimateDevices();
return {
success: true,
climate_devices: devices,
count: devices.length
};
}
/**
* Get a specific climate device
*/
private getClimateDevice(entity_id: string): Record<string, unknown> {
const device = haClimateService.getClimateDevice(entity_id);
if (!device) {
return {
success: false,
error: `Climate device ${entity_id} not found`
};
}
return {
success: true,
device
};
}
/**
* Set HVAC mode
*/
private setHVACMode(entity_id: string, hvac_mode: string): Record<string, unknown> {
const success = haClimateService.setHVACMode(entity_id, hvac_mode);
if (!success) {
return {
success: false,
error: `Failed to set HVAC mode for ${entity_id}: device not found or mode not supported`
};
}
const device = haClimateService.getClimateDevice(entity_id);
return {
success: true,
message: `Set HVAC mode to ${hvac_mode} for ${entity_id}`,
device
};
}
/**
* Set temperature
*/
private setTemperature(
entity_id: string,
temperature?: number,
target_temp_high?: number,
target_temp_low?: number
): Record<string, unknown> {
const success = haClimateService.setTemperature(
entity_id,
temperature,
target_temp_high,
target_temp_low
);
if (!success) {
return {
success: false,
error: `Failed to set temperature for ${entity_id}: device not found or feature not supported`
};
}
const device = haClimateService.getClimateDevice(entity_id);
const tempMessage = temperature !== undefined
? `temperature to ${temperature}°`
: `temperature range to ${target_temp_low}° - ${target_temp_high}°`;
return {
success: true,
message: `Set ${tempMessage} for ${entity_id}`,
device
};
}
/**
* Set fan mode
*/
private setFanMode(entity_id: string, fan_mode: string): Record<string, unknown> {
const success = haClimateService.setFanMode(entity_id, fan_mode);
if (!success) {
return {
success: false,
error: `Failed to set fan mode for ${entity_id}: device not found or mode not supported`
};
}
const device = haClimateService.getClimateDevice(entity_id);
return {
success: true,
message: `Set fan mode to ${fan_mode} for ${entity_id}`,
device
};
}
}

View File

@@ -0,0 +1,327 @@
/**
* Lights Control Tool for Home Assistant
*
* This tool allows controlling lights in Home Assistant through the MCP.
* It supports turning lights on/off, changing brightness, color, and color temperature.
*/
import { z } from "zod";
import { BaseTool } from "../base-tool.js";
import { logger } from "../../utils/logger.js";
import { MCPContext } from "../../mcp/types.js";
// Mock Home Assistant API service in absence of actual HA integration
class MockHALightsService {
private lights: Map<string, {
state: "on" | "off";
brightness?: number;
color_temp?: number;
rgb_color?: [number, number, number];
friendly_name: string;
}>;
constructor() {
// Initialize with some mock lights
this.lights = new Map([
["light.living_room", {
state: "off",
brightness: 255,
friendly_name: "Living Room Light"
}],
["light.kitchen", {
state: "on",
brightness: 200,
friendly_name: "Kitchen Light"
}],
["light.bedroom", {
state: "off",
brightness: 150,
color_temp: 400,
friendly_name: "Bedroom Light"
}],
["light.office", {
state: "on",
brightness: 255,
rgb_color: [255, 255, 255],
friendly_name: "Office Light"
}]
]);
}
// Get all lights
public getLights(): Record<string, unknown>[] {
const result = [];
for (const [entity_id, light] of this.lights.entries()) {
result.push({
entity_id,
state: light.state,
attributes: {
...light,
friendly_name: light.friendly_name
}
});
}
return result;
}
// Get a specific light
public getLight(entity_id: string): Record<string, unknown> | null {
const light = this.lights.get(entity_id);
if (!light) {
return null;
}
return {
entity_id,
state: light.state,
attributes: {
...light,
friendly_name: light.friendly_name
}
};
}
// Turn a light on
public turnOn(entity_id: string, attributes: Record<string, unknown> = {}): boolean {
const light = this.lights.get(entity_id);
if (!light) {
return false;
}
light.state = "on";
// Apply attributes
if (typeof attributes.brightness === "number") {
light.brightness = Math.max(0, Math.min(255, attributes.brightness));
}
if (typeof attributes.color_temp === "number") {
light.color_temp = Math.max(153, Math.min(500, attributes.color_temp));
}
if (Array.isArray(attributes.rgb_color) && attributes.rgb_color.length >= 3) {
// Individually extract and validate each RGB component
const r = Number(attributes.rgb_color[0]);
const g = Number(attributes.rgb_color[1]);
const b = Number(attributes.rgb_color[2]);
// Only set if we got valid numbers
if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
light.rgb_color = [
Math.max(0, Math.min(255, r)),
Math.max(0, Math.min(255, g)),
Math.max(0, Math.min(255, b))
];
}
}
this.lights.set(entity_id, light);
return true;
}
// Turn a light off
public turnOff(entity_id: string): boolean {
const light = this.lights.get(entity_id);
if (!light) {
return false;
}
light.state = "off";
this.lights.set(entity_id, light);
return true;
}
}
// Singleton instance
const haLightsService = new MockHALightsService();
// Define the schema for our tool parameters
const lightsControlSchema = z.object({
action: z.enum(["list", "get", "turn_on", "turn_off"]).describe("The action to perform"),
entity_id: z.string().optional().describe("The entity ID of the light to control"),
brightness: z.number().min(0).max(255).optional().describe("Brightness level (0-255)"),
color_temp: z.number().min(153).max(500).optional().describe("Color temperature (153-500)"),
rgb_color: z.tuple([
z.number().min(0).max(255),
z.number().min(0).max(255),
z.number().min(0).max(255)
]).optional().describe("RGB color as [r, g, b]"),
});
type LightsControlParams = z.infer<typeof lightsControlSchema>;
/**
* Tool for controlling lights in Home Assistant
*/
export class LightsControlTool extends BaseTool {
constructor() {
super({
name: "lights_control",
description: "Control lights in Home Assistant",
parameters: lightsControlSchema,
metadata: {
category: "home_assistant",
version: "1.0.0",
tags: ["lights", "home_assistant", "control"],
examples: [
{
description: "List all lights",
params: { action: "list" }
},
{
description: "Turn on a light with brightness",
params: {
action: "turn_on",
entity_id: "light.living_room",
brightness: 200
}
}
]
}
});
}
/**
* Execute the tool
*/
public async execute(params: LightsControlParams, context: MCPContext): Promise<Record<string, unknown>> {
logger.debug(`Executing LightsControlTool with params: ${JSON.stringify(params)}`);
try {
// Add an await here to satisfy the linter
await Promise.resolve();
// Pre-declare variables that will be used in the switch statement
let attributes: Record<string, unknown>;
switch (params.action) {
case "list":
return this.listLights();
case "get":
if (!params.entity_id) {
throw new Error("entity_id is required for get action");
}
return this.getLight(params.entity_id);
case "turn_on":
if (!params.entity_id) {
throw new Error("entity_id is required for turn_on action");
}
// Initialize attributes outside the case block
attributes = {};
if (params.brightness !== undefined) {
attributes.brightness = params.brightness;
}
if (params.color_temp !== undefined) {
attributes.color_temp = params.color_temp;
}
if (params.rgb_color !== undefined) {
// Ensure the rgb_color is passed correctly
attributes.rgb_color = [
params.rgb_color[0],
params.rgb_color[1],
params.rgb_color[2]
];
}
return this.turnOnLight(params.entity_id, attributes);
case "turn_off":
if (!params.entity_id) {
throw new Error("entity_id is required for turn_off action");
}
return this.turnOffLight(params.entity_id);
default:
throw new Error(`Unknown action: ${String(params.action)}`);
}
} catch (error) {
logger.error(`Error in LightsControlTool: ${String(error)}`);
throw error;
}
}
/**
* List all available lights
*/
private listLights(): Record<string, unknown> {
const lights = haLightsService.getLights();
return {
success: true,
lights,
count: lights.length
};
}
/**
* Get a specific light
*/
private getLight(entity_id: string): Record<string, unknown> {
const light = haLightsService.getLight(entity_id);
if (!light) {
return {
success: false,
error: `Light ${entity_id} not found`
};
}
return {
success: true,
light
};
}
/**
* Turn on a light
*/
private turnOnLight(
entity_id: string,
attributes: Record<string, unknown>
): Record<string, unknown> {
const success = haLightsService.turnOn(entity_id, attributes);
if (!success) {
return {
success: false,
error: `Failed to turn on ${entity_id}: light not found`
};
}
const light = haLightsService.getLight(entity_id);
return {
success: true,
message: `Turned on ${entity_id}`,
light
};
}
/**
* Turn off a light
*/
private turnOffLight(entity_id: string): Record<string, unknown> {
const success = haLightsService.turnOff(entity_id);
if (!success) {
return {
success: false,
error: `Failed to turn off ${entity_id}: light not found`
};
}
const light = haLightsService.getLight(entity_id);
return {
success: true,
message: `Turned off ${entity_id}`,
light
};
}
}

View File

@@ -1,112 +1,77 @@
/**
* Logging Module
*
* This module provides logging functionality with rotation support.
* It uses winston for logging and winston-daily-rotate-file for rotation.
*
* @module logger
* Logger Module
*
* This module provides a consistent logging interface for all MCP components.
* It handles log formatting, error handling, and ensures log output is directed
* to the appropriate destination based on the runtime environment.
*/
import winston from "winston";
import DailyRotateFile from "winston-daily-rotate-file";
import { APP_CONFIG } from "../config/app.config.js";
import winston from 'winston';
import path from 'path';
import fs from 'fs';
/**
* Log levels configuration
* Defines the severity levels for logging
*/
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
// Ensure logs directory exists
const logsDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
/**
* Log level colors configuration
* Defines colors for different log levels
*/
const colors = {
error: "red",
warn: "yellow",
info: "green",
http: "magenta",
debug: "white",
};
// Special handling for stdio mode to ensure stdout stays clean for JSON-RPC
const isStdioMode = process.env.USE_STDIO_TRANSPORT === 'true';
const isDebugStdio = process.env.DEBUG_STDIO === 'true';
/**
* Add colors to winston
*/
winston.addColors(colors);
/**
* Log format configuration
* Defines how log messages are formatted
*/
const format = winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss:ms" }),
winston.format.colorize({ all: true }),
winston.format.printf(
(info) => `${info.timestamp} ${info.level}: ${info.message}`,
),
// Create base format that works with TypeScript
const baseFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json()
);
/**
* Transport for daily rotating file
* Configures how logs are rotated and stored
*/
const dailyRotateFileTransport = new DailyRotateFile({
filename: "logs/%DATE%.log",
datePattern: "YYYY-MM-DD",
zippedArchive: true,
maxSize: "20m",
maxFiles: "14d",
format: winston.format.combine(
winston.format.uncolorize(),
winston.format.timestamp(),
winston.format.json(),
),
});
/**
* Transport for error logs
* Stores error logs in a separate file
*/
const errorFileTransport = new DailyRotateFile({
filename: "logs/error-%DATE%.log",
datePattern: "YYYY-MM-DD",
level: "error",
zippedArchive: true,
maxSize: "20m",
maxFiles: "14d",
format: winston.format.combine(
winston.format.uncolorize(),
winston.format.timestamp(),
winston.format.json(),
),
});
/**
* Create the logger instance
*/
// Create logger with appropriate transports
const logger = winston.createLogger({
level: APP_CONFIG.NODE_ENV === "development" ? "debug" : "info",
levels,
format,
level: process.env.LOG_LEVEL || 'error',
format: baseFormat,
defaultMeta: { service: 'mcp-server' },
transports: [
new winston.transports.Console({
// Always log to files
new winston.transports.File({ filename: path.join(logsDir, 'error.log'), level: 'error' }),
new winston.transports.File({ filename: path.join(logsDir, 'combined.log') })
]
});
// Handle console output based on environment
if (process.env.NODE_ENV !== 'production' || process.env.CONSOLE_LOGGING === 'true') {
// In stdio mode with debug enabled, ensure logs only go to stderr to keep stdout clean for JSON-RPC
if (isStdioMode && isDebugStdio) {
// Use stderr stream transport in stdio debug mode
logger.add(new winston.transports.Stream({
stream: process.stderr,
format: winston.format.combine(
winston.format.simple()
)
}));
} else {
// Use console transport in normal mode
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
),
}),
dailyRotateFileTransport,
errorFileTransport,
],
});
winston.format.simple()
)
}));
}
}
/**
* Export the logger instance
*/
// Custom logger interface
export interface MCPLogger {
debug: (message: string, meta?: Record<string, any>) => void;
info: (message: string, meta?: Record<string, any>) => void;
warn: (message: string, meta?: Record<string, any>) => void;
error: (message: string, meta?: Record<string, any>) => void;
child: (options: Record<string, any>) => MCPLogger;
}
// Export the winston logger with MCPLogger interface
export { logger };
// Export default logger for convenience
export default logger;

View File

@@ -0,0 +1,339 @@
/**
* Stdio Transport Module
*
* This module implements communication via standard input/output streams
* using JSON-RPC 2.0 format for sending and receiving messages.
*
* @module stdio-transport
*/
import { createInterface } from "readline";
import { logger } from "./logger.js";
import { z } from "zod";
// JSON-RPC 2.0 error codes
export enum JsonRpcErrorCode {
// Standard JSON-RPC 2.0 error codes
PARSE_ERROR = -32700,
INVALID_REQUEST = -32600,
METHOD_NOT_FOUND = -32601,
INVALID_PARAMS = -32602,
INTERNAL_ERROR = -32603,
// MCP specific error codes
TOOL_EXECUTION_ERROR = -32000,
VALIDATION_ERROR = -32001,
}
// Type definitions for JSON-RPC 2.0 messages
export interface JsonRpcRequest {
jsonrpc: "2.0";
id: string | number;
method: string;
params?: Record<string, unknown>;
}
export interface JsonRpcResponse {
jsonrpc: "2.0";
id: string | number;
result?: unknown;
error?: JsonRpcError;
}
export interface JsonRpcError {
code: number;
message: string;
data?: unknown;
}
export interface JsonRpcNotification {
jsonrpc: "2.0";
method: string;
params?: Record<string, unknown>;
}
// Setup readline interface for stdin
const rl = createInterface({
input: process.stdin,
terminal: false
});
// Message handlers map
const messageHandlers: Map<string, {
execute: (params: Record<string, unknown>) => Promise<unknown>;
paramsSchema?: z.ZodType<any>;
}> = new Map();
/**
* Initialize stdio transport
* Sets up event listeners and message processing
*/
export function initStdioTransport(): void {
// Check for silent startup mode
const silentStartup = process.env.SILENT_STARTUP === 'true';
// Handle line events (incoming JSON)
rl.on('line', async (line) => {
try {
// Parse incoming JSON
const request = JSON.parse(line);
// Validate it's a proper JSON-RPC 2.0 request
if (!request.jsonrpc || request.jsonrpc !== "2.0") {
sendErrorResponse({
id: request.id || null,
code: JsonRpcErrorCode.INVALID_REQUEST,
message: "Invalid JSON-RPC 2.0 request: missing or invalid jsonrpc version"
});
return;
}
// Handle request with ID (requires response)
if (request.id !== undefined) {
await handleJsonRpcRequest(request as JsonRpcRequest).catch(err => {
if (!silentStartup) {
logger.error(`Error handling request: ${String(err)}`);
}
});
}
// Handle notification (no response expected)
else if (request.method) {
void handleJsonRpcNotification(request as JsonRpcNotification);
}
// Invalid request format
else {
sendErrorResponse({
id: null,
code: JsonRpcErrorCode.INVALID_REQUEST,
message: "Invalid JSON-RPC 2.0 message format"
});
}
} catch (parseError) {
// Handle JSON parsing errors
if (!silentStartup) {
logger.error(`Failed to parse JSON input: ${String(parseError)}`);
}
sendErrorResponse({
id: null,
code: JsonRpcErrorCode.PARSE_ERROR,
message: "Parse error: invalid JSON",
data: parseError instanceof Error ? parseError.message : String(parseError)
});
}
});
// Handle stdin close
rl.on('close', () => {
if (!silentStartup) {
logger.info('Stdin closed, shutting down');
}
process.exit(0);
});
// Log initialization only if not in silent mode
if (!silentStartup) {
logger.info("JSON-RPC 2.0 stdio transport initialized");
}
}
/**
* Handle a JSON-RPC request that requires a response
*/
async function handleJsonRpcRequest(request: JsonRpcRequest): Promise<void> {
const { id, method, params = {} } = request;
// Log to file but not console
logger.debug(`Received request: ${id} - ${method}`);
// Look up handler
const handler = messageHandlers.get(method);
if (!handler) {
sendErrorResponse({
id,
code: JsonRpcErrorCode.METHOD_NOT_FOUND,
message: `Method not found: ${method}`
});
return;
}
try {
// Validate parameters if schema exists
if (handler.paramsSchema) {
try {
const validationResult = handler.paramsSchema.parse(params);
// If validation changes values (e.g. default values), use the validated result
Object.assign(params, validationResult);
} catch (validationError) {
sendErrorResponse({
id,
code: JsonRpcErrorCode.INVALID_PARAMS,
message: "Invalid parameters",
data: validationError instanceof Error ? validationError.message : String(validationError)
});
return;
}
}
// Execute handler
const result = await handler.execute(params);
// Send successful response
sendResponse({
id,
result
});
} catch (error) {
// Handle execution errors
sendErrorResponse({
id,
code: JsonRpcErrorCode.TOOL_EXECUTION_ERROR,
message: error instanceof Error ? error.message : String(error),
data: error
});
}
}
/**
* Handle a JSON-RPC notification (no response required)
*/
async function handleJsonRpcNotification(notification: JsonRpcNotification): Promise<void> {
const { method, params = {} } = notification;
// Log to file but not console
logger.debug(`Received notification: ${method}`);
// Look up handler
const handler = messageHandlers.get(method);
if (!handler) {
// No response for notifications even if method not found
logger.warn(`Method not found for notification: ${method}`);
return;
}
try {
// Validate parameters if schema exists
if (handler.paramsSchema) {
try {
handler.paramsSchema.parse(params);
} catch (validationError) {
logger.error(`Invalid parameters for notification ${method}: ${String(validationError)}`);
return;
}
}
// Execute handler (fire and forget)
await handler.execute(params);
} catch (error) {
// Log execution errors but don't send response
logger.error(`Error handling notification ${method}: ${String(error)}`);
}
}
/**
* Register a message handler for a specific method
*
* @param method - The method name to handle
* @param handler - The function to handle the method
* @param paramsSchema - Optional Zod schema for parameter validation
*/
export function registerHandler(
method: string,
handler: (params: Record<string, unknown>) => Promise<unknown>,
paramsSchema?: z.ZodType<any>
): void {
messageHandlers.set(method, {
execute: handler,
paramsSchema
});
logger.debug(`Registered handler for method: ${method}`);
}
/**
* Send a successful response to stdout
*
* @param options - The response options
*/
export function sendResponse({ id, result }: { id: string | number; result?: unknown }): void {
const response: JsonRpcResponse = {
jsonrpc: "2.0",
id,
result
};
const jsonResponse = JSON.stringify(response);
process.stdout.write(jsonResponse + '\n');
logger.debug(`Sent response: ${id}`);
}
/**
* Send an error response to stdout
*
* @param error - The error details
*/
export function sendErrorResponse({
id,
code,
message,
data
}: {
id: string | number | null;
code: number;
message: string;
data?: unknown;
}): void {
const response: JsonRpcResponse = {
jsonrpc: "2.0",
id: id ?? null,
error: {
code,
message,
data
}
};
const jsonResponse = JSON.stringify(response);
process.stdout.write(jsonResponse + '\n');
logger.error(`Sent error response: ${id} - [${code}] ${message}`);
}
/**
* Send a notification to the client (no response expected)
*
* @param method - The notification method name
* @param params - The notification parameters
*/
export function sendNotification(method: string, params?: Record<string, unknown>): void {
const notification: JsonRpcNotification = {
jsonrpc: "2.0",
method,
params
};
const jsonNotification = JSON.stringify(notification);
process.stdout.write(jsonNotification + '\n');
logger.debug(`Sent notification: ${method}`);
}
/**
* Send a log message to the client
*
* @param level - The log level (info, warn, error, debug)
* @param message - The log message
* @param data - Optional additional data
*/
export function sendLogMessage(level: string, message: string, data?: unknown): void {
sendNotification("log", {
level,
message,
data,
timestamp: new Date().toISOString()
});
}
/**
* Enable debug mode for the transport
* Increases logging verbosity
*/
export function enableDebugMode(): void {
logger.level = "debug";
logger.info("Debug mode enabled for stdio transport");
}

97
stdio-start.sh Executable file
View File

@@ -0,0 +1,97 @@
#!/bin/bash
# MCP Server Stdio Transport Launcher
# This script builds and runs the MCP server using stdin/stdout JSON-RPC 2.0 transport
# ANSI colors for prettier output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Show usage information
function show_usage {
echo -e "${BLUE}Usage:${NC} $0 [options]"
echo
echo "Options:"
echo " --debug Enable debug mode"
echo " --rebuild Force rebuild even if dist exists"
echo " --help Show this help message"
echo
echo "Examples:"
echo " $0 # Normal start"
echo " $0 --debug # Start with debug logging"
echo " $0 --rebuild # Force rebuild"
echo
echo "This script runs the MCP server with JSON-RPC 2.0 stdio transport."
echo "Logs will be written to the logs directory but not to stdout."
echo
}
# Process command line arguments
REBUILD=false
DEBUG=false
for arg in "$@"; do
case $arg in
--help)
show_usage
exit 0
;;
--debug)
DEBUG=true
shift
;;
--rebuild)
REBUILD=true
shift
;;
*)
echo -e "${RED}Unknown option:${NC} $arg"
show_usage
exit 1
;;
esac
done
# Check for errors
if [ ! -f ".env" ]; then
echo -e "${RED}Error:${NC} .env file not found. Please create one from .env.example." >&2
exit 1
fi
# Set environment variables
export USE_STDIO_TRANSPORT=true
# Set debug mode if requested
if [ "$DEBUG" = true ]; then
export DEBUG=true
echo -e "${YELLOW}Debug mode enabled${NC}" >&2
fi
# Check if we need to build
if [ ! -d "dist" ] || [ "$REBUILD" = true ]; then
echo -e "${BLUE}Building MCP server with stdio transport...${NC}" >&2
bun build ./src/index.ts --outdir ./dist --target bun || {
echo -e "${RED}Build failed!${NC}" >&2
exit 1
}
else
echo -e "${GREEN}Using existing build in dist/ directory${NC}" >&2
echo -e "${YELLOW}Use --rebuild flag to force a rebuild${NC}" >&2
fi
# Create logs directory if it doesn't exist
mkdir -p logs
# Run the application with stdio transport
echo -e "${GREEN}Starting MCP server with stdio transport...${NC}" >&2
echo -e "${YELLOW}Note: All logs will be written to logs/ directory${NC}" >&2
echo -e "${YELLOW}Press Ctrl+C to stop${NC}" >&2
# Execute the server
exec bun run dist/index.js
# The exec replaces this shell with the server process
# so any code after this point will not be executed

146
test-jsonrpc.js Executable file
View File

@@ -0,0 +1,146 @@
#!/usr/bin/env node
/**
* JSON-RPC 2.0 Test Script for MCP Server
*
* This script tests the stdio transport communication with the MCP server
* by sending JSON-RPC 2.0 requests and processing responses.
*
* Usage:
* ./stdio-start.sh | node test-jsonrpc.js
* or
* node test-jsonrpc.js < sample-responses.json
*/
const { spawn } = require('child_process');
const readline = require('readline');
// Generate a random request ID
const generateId = () => Math.random().toString(36).substring(2, 15);
// Counter for keeping track of requests/responses
let messageCount = 0;
let pendingRequests = new Map();
// Set up readline interface for stdin
const rl = readline.createInterface({
input: process.stdin,
terminal: false
});
// Handle responses from the MCP server
rl.on('line', (line) => {
try {
const response = JSON.parse(line);
messageCount++;
console.log(`\n[RECEIVED] Response #${messageCount}:`);
console.dir(response, { depth: null, colors: true });
// Check if this is a notification
if (!response.id && response.method) {
console.log(`👉 Received notification: ${response.method}`);
return;
}
// Check if this is a response to a pending request
if (response.id && pendingRequests.has(response.id)) {
const requestTime = pendingRequests.get(response.id);
const responseTime = Date.now();
console.log(`⏱️ Response time: ${responseTime - requestTime}ms`);
pendingRequests.delete(response.id);
}
// Check for error
if (response.error) {
console.log(`❌ Error [${response.error.code}]: ${response.error.message}`);
} else if (response.result) {
console.log(`✅ Success`);
}
} catch (error) {
console.error(`Error parsing response: ${error.message}`);
console.error(`Raw response: ${line}`);
}
});
// Define test requests
const testRequests = [
// Test valid request
{
jsonrpc: "2.0",
id: generateId(),
method: "listDevicesTool",
params: {
entity_type: "light"
}
},
// Test method not found
{
jsonrpc: "2.0",
id: generateId(),
method: "nonexistentMethod",
params: {}
},
// Test invalid params
{
jsonrpc: "2.0",
id: generateId(),
method: "controlTool",
params: {
// Missing required parameters
}
},
// Test notification (no response expected)
{
jsonrpc: "2.0",
method: "ping",
params: {
timestamp: Date.now()
}
},
// Test malformed request (missing jsonrpc version)
{
id: generateId(),
method: "listDevicesTool",
params: {}
}
];
// Send requests with delay between each
let requestIndex = 0;
function sendNextRequest() {
if (requestIndex >= testRequests.length) {
console.log('\n✨ All test requests sent!');
return;
}
const request = testRequests[requestIndex++];
console.log(`\n[SENDING] Request #${requestIndex}:`);
console.dir(request, { depth: null, colors: true });
// Store the request time for calculating response time
if (request.id) {
pendingRequests.set(request.id, Date.now());
}
// Send the request to the MCP server
process.stdout.write(JSON.stringify(request) + '\n');
// Schedule the next request
setTimeout(sendNextRequest, 1000);
}
// Start sending test requests after a delay to allow server initialization
console.log('🚀 Starting JSON-RPC 2.0 test...');
setTimeout(sendNextRequest, 2000);
// Handle Ctrl+C
process.on('SIGINT', () => {
console.log('\n👋 Test script terminated');
process.exit(0);
});

19
tsconfig.stdio.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"sourceMap": true
},
"include": [
"src/stdio-server.ts",
"src/mcp/**/*.ts",
"src/utils/**/*.ts",
"src/tools/homeassistant/**/*.ts"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts"
]
}

48
webpack.config.cjs Normal file
View File

@@ -0,0 +1,48 @@
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production',
target: 'node',
entry: './src/utils/stdio-transport.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'stdio-transport.js',
library: {
type: 'commonjs2'
}
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
extensionAlias: {
'.js': ['.js', '.ts'],
'.cjs': ['.cjs', '.cts'],
'.mjs': ['.mjs', '.mts']
}
},
optimization: {
minimize: true,
minimizer: [new TerserPlugin({
terserOptions: {
format: {
comments: false,
},
},
extractComments: false,
})],
},
externals: {
// Mark node modules as external to reduce bundle size
'express': 'commonjs express',
'winston': 'commonjs winston'
}
};