@1mcp/agent 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -50
- package/build/capabilities/capabilityManager.js +2 -5
- package/build/client.js +4 -3
- package/build/clients/clientManager.js +8 -4
- package/build/config/configManager.js +30 -8
- package/build/config/transportConfig.js +50 -14
- package/build/constants.js +31 -1
- package/build/handlers/notificationHandlers.js +1 -1
- package/build/handlers/requestHandlers.js +2 -2
- package/build/index.js +36 -98
- package/build/{logger.js → logger/logger.js} +7 -3
- package/build/{mcpTransport.js → logger/mcpTransport.js} +33 -5
- package/build/middleware/loggingMiddleware.js +114 -0
- package/build/server/expressServer.js +94 -0
- package/build/server.js +9 -42
- package/build/serverManager.js +71 -0
- package/build/services/configReloadService.js +12 -19
- package/build/utils/errorHandling.js +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -4,47 +4,37 @@ A unified Model Context Protocol server implementation that aggregates multiple
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
1MCP (One MCP) is designed to simplify the way you work with AI
|
|
7
|
+
1MCP (One MCP) is designed to simplify the way you work with AI assistants. Instead of configuring multiple MCP servers for different clients (Claude Desktop, Cherry Studio, Cursor, Roo Code, Claude, etc.), 1MCP provides a single, unified server that:
|
|
8
8
|
|
|
9
9
|
- Aggregates multiple MCP servers into one unified interface
|
|
10
10
|
- Reduces system resource usage by eliminating redundant server instances
|
|
11
|
-
- Simplifies configuration management across different AI
|
|
11
|
+
- Simplifies configuration management across different AI assistants
|
|
12
12
|
- Provides a standardized way for AI models to interact with external tools and resources
|
|
13
|
+
- Supports dynamic configuration reloading without server restart
|
|
14
|
+
- Handles graceful shutdown and resource cleanup
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
## Quick Start
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
- Winston-based logging system with MCP protocol integration
|
|
18
|
-
- Support for MCP capabilities and notifications
|
|
19
|
-
- Smart request routing and capability aggregation
|
|
18
|
+
To enable Cursor to use existing MCP servers already configured in Claude Desktop, follow these steps:
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- Express.js for HTTP handling
|
|
26
|
-
- Server-Sent Events (SSE) for real-time communication
|
|
27
|
-
- MCP SDK for protocol implementation
|
|
28
|
-
|
|
29
|
-
### Server Configuration
|
|
30
|
-
|
|
31
|
-
The server runs on port 3050 by default and exposes the following endpoints:
|
|
32
|
-
|
|
33
|
-
- `/sse` - SSE endpoint for establishing connections
|
|
34
|
-
- `/messages` - Endpoint for receiving messages from clients
|
|
35
|
-
|
|
36
|
-
## MCP Logging Implementation
|
|
37
|
-
|
|
38
|
-
This project includes a Winston transport for the Model Context Protocol (MCP) logging protocol. The implementation allows logs from the agent to be sent to MCP clients using the standard MCP logging notification format.
|
|
20
|
+
1. Run the 1MCP server with the Claude Desktop config file:
|
|
21
|
+
```bash
|
|
22
|
+
npx -y @1mcp/agent --config ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
|
23
|
+
```
|
|
39
24
|
|
|
40
|
-
|
|
25
|
+
2. Add the 1MCP server to your Cursor config file (`~/.cursor/mcp.json`):
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"1mcp": {
|
|
30
|
+
"type": "http",
|
|
31
|
+
"url": "http://localhost:3050"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
- Automatic mapping of Winston log levels to MCP log levels
|
|
44
|
-
- Support for structured logging data
|
|
45
|
-
- Configurable logger name
|
|
46
|
-
- Connection-aware logging (only sends logs when clients are connected)
|
|
47
|
-
- Sensitive data sanitization
|
|
37
|
+
3. Enjoy it!
|
|
48
38
|
|
|
49
39
|
## Usage
|
|
50
40
|
|
|
@@ -57,14 +47,107 @@ npx -y @1mcp/agent
|
|
|
57
47
|
# Run with stdio transport
|
|
58
48
|
npx -y @1mcp/agent --transport stdio
|
|
59
49
|
|
|
50
|
+
# Run with AI assistant config file
|
|
51
|
+
npx -y @1mcp/agent --config /path/to/ai/assistant/config.json
|
|
52
|
+
|
|
60
53
|
# Show help
|
|
61
54
|
npx -y @1mcp/agent --help
|
|
62
55
|
```
|
|
63
56
|
|
|
64
57
|
Available options:
|
|
65
|
-
- `--transport, -t`: Transport type to use (choices: "stdio", "
|
|
58
|
+
- `--transport, -t`: Transport type to use (choices: "stdio", "sse", default: "sse")
|
|
59
|
+
- `--config, -c`: Path to the configuration file
|
|
66
60
|
- `--help, -h`: Show help
|
|
67
61
|
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
### Global Configuration
|
|
65
|
+
|
|
66
|
+
The server automatically manages configuration in a global location:
|
|
67
|
+
|
|
68
|
+
- macOS/Linux: `~/.config/1mcp/mcp.json`
|
|
69
|
+
- Windows: `%APPDATA%/1mcp/mcp.json`
|
|
70
|
+
|
|
71
|
+
### Configuration File Format
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"mcp-server-fetch": {
|
|
77
|
+
"command": "uvx",
|
|
78
|
+
"args": [
|
|
79
|
+
"mcp-server-fetch"
|
|
80
|
+
],
|
|
81
|
+
"disabled": false
|
|
82
|
+
},
|
|
83
|
+
"server-memory": {
|
|
84
|
+
"command": "npx",
|
|
85
|
+
"args": [
|
|
86
|
+
"-y",
|
|
87
|
+
"@modelcontextprotocol/server-memory"
|
|
88
|
+
],
|
|
89
|
+
"disabled": false
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## How It Works
|
|
96
|
+
|
|
97
|
+
### System Architecture
|
|
98
|
+
|
|
99
|
+
```mermaid
|
|
100
|
+
graph TB
|
|
101
|
+
subgraph "AI Assistants"
|
|
102
|
+
A1[Claude Desktop]
|
|
103
|
+
A2[Cursor]
|
|
104
|
+
A3[Cherry Studio]
|
|
105
|
+
A4[Roo Code]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
subgraph "1MCP Server"
|
|
109
|
+
MCP[1MCP Agent]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
subgraph "MCP Servers"
|
|
113
|
+
S1[Server 1]
|
|
114
|
+
S2[Server 2]
|
|
115
|
+
S3[Server 3]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
A1 -->|sse| MCP
|
|
119
|
+
A2 -->|sse| MCP
|
|
120
|
+
A3 -->|sse| MCP
|
|
121
|
+
A4 -->|sse| MCP
|
|
122
|
+
|
|
123
|
+
MCP --> |sse| S1
|
|
124
|
+
MCP --> |stdio| S2
|
|
125
|
+
MCP --> |stdio| S3
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Request Flow
|
|
129
|
+
|
|
130
|
+
```mermaid
|
|
131
|
+
sequenceDiagram
|
|
132
|
+
participant Client as AI Assistant
|
|
133
|
+
participant 1MCP as 1MCP Server
|
|
134
|
+
participant MCP as MCP Servers
|
|
135
|
+
|
|
136
|
+
Client->>1MCP: Send MCP Request
|
|
137
|
+
activate 1MCP
|
|
138
|
+
|
|
139
|
+
1MCP->>1MCP: Validate Request
|
|
140
|
+
1MCP->>1MCP: Load Config
|
|
141
|
+
1MCP->>MCP: Forward Request
|
|
142
|
+
activate MCP
|
|
143
|
+
|
|
144
|
+
MCP-->>1MCP: Response
|
|
145
|
+
deactivate MCP
|
|
146
|
+
|
|
147
|
+
1MCP-->>Client: Forward Response
|
|
148
|
+
deactivate 1MCP
|
|
149
|
+
```
|
|
150
|
+
|
|
68
151
|
## Development
|
|
69
152
|
|
|
70
153
|
Install dependencies:
|
|
@@ -87,23 +170,6 @@ Run the server:
|
|
|
87
170
|
pnpm dev
|
|
88
171
|
```
|
|
89
172
|
|
|
90
|
-
## Installation
|
|
91
|
-
|
|
92
|
-
To use with Claude Desktop, add the server config:
|
|
93
|
-
|
|
94
|
-
On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
95
|
-
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
|
|
96
|
-
|
|
97
|
-
```json
|
|
98
|
-
{
|
|
99
|
-
"mcpServers": {
|
|
100
|
-
"agent": {
|
|
101
|
-
"command": "/path/to/agent/build/index.js"
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
173
|
### Debugging
|
|
108
174
|
|
|
109
175
|
Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import logger from '../logger.js';
|
|
1
|
+
import logger from '../logger/logger.js';
|
|
2
2
|
import { setupClientToServerNotifications, setupServerToClientNotifications, } from '../handlers/notificationHandlers.js';
|
|
3
3
|
import { registerRequestHandlers } from '../handlers/requestHandlers.js';
|
|
4
4
|
/**
|
|
@@ -7,15 +7,12 @@ import { registerRequestHandlers } from '../handlers/requestHandlers.js';
|
|
|
7
7
|
* @param server The MCP server instance
|
|
8
8
|
* @returns The combined server capabilities
|
|
9
9
|
*/
|
|
10
|
-
export async function
|
|
10
|
+
export async function setupCapabilities(clients, server) {
|
|
11
11
|
// Collect capabilities from all clients
|
|
12
12
|
const capabilities = collectCapabilities(clients);
|
|
13
13
|
// Set up notification handlers
|
|
14
14
|
setupClientToServerNotifications(clients, server);
|
|
15
15
|
setupServerToClientNotifications(clients, server);
|
|
16
|
-
// Register capabilities with the server
|
|
17
|
-
logger.info(`Registering capabilities: ${JSON.stringify(capabilities)}`);
|
|
18
|
-
server.registerCapabilities(capabilities);
|
|
19
16
|
// Register request handlers based on capabilities
|
|
20
17
|
registerRequestHandlers(clients, server, capabilities);
|
|
21
18
|
return capabilities;
|
package/build/client.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
-
|
|
2
|
+
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from './constants.js';
|
|
3
|
+
export default async function createClient() {
|
|
3
4
|
const client = new Client({
|
|
4
|
-
name:
|
|
5
|
-
version:
|
|
5
|
+
name: MCP_SERVER_NAME,
|
|
6
|
+
version: MCP_SERVER_VERSION,
|
|
6
7
|
}, {
|
|
7
8
|
capabilities: {
|
|
8
9
|
experimental: {},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import createClient from '../client.js';
|
|
2
|
-
import logger from '../logger.js';
|
|
3
|
-
import { CONNECTION_RETRY } from '../constants.js';
|
|
2
|
+
import logger from '../logger/logger.js';
|
|
3
|
+
import { CONNECTION_RETRY, MCP_SERVER_NAME } from '../constants.js';
|
|
4
4
|
import { ClientConnectionError, ClientNotFoundError, withErrorHandling } from '../utils/errorHandling.js';
|
|
5
5
|
/**
|
|
6
6
|
* Creates client instances for all transports with retry logic
|
|
@@ -12,7 +12,7 @@ export async function createClients(transports) {
|
|
|
12
12
|
for (const [name, transport] of Object.entries(transports)) {
|
|
13
13
|
logger.info(`Creating client for ${name}`);
|
|
14
14
|
try {
|
|
15
|
-
const client = await createClient(
|
|
15
|
+
const client = await createClient();
|
|
16
16
|
// Connect with retry logic
|
|
17
17
|
await connectWithRetry(client, transport, name);
|
|
18
18
|
clients[name] = client;
|
|
@@ -36,7 +36,11 @@ async function connectWithRetry(client, transport, name) {
|
|
|
36
36
|
for (let i = 0; i < CONNECTION_RETRY.MAX_ATTEMPTS; i++) {
|
|
37
37
|
try {
|
|
38
38
|
await client.connect(transport);
|
|
39
|
-
|
|
39
|
+
const sv = await client.getServerVersion();
|
|
40
|
+
if (sv?.name === MCP_SERVER_NAME) {
|
|
41
|
+
throw new ClientConnectionError(name, new Error('Aborted to prevent circular dependency'));
|
|
42
|
+
}
|
|
43
|
+
logger.info(`Successfully connected to ${name} with server ${sv?.name} version ${sv?.version}`);
|
|
40
44
|
return;
|
|
41
45
|
}
|
|
42
46
|
catch (error) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { EventEmitter } from 'events';
|
|
4
|
-
import {
|
|
5
|
-
import logger from '../logger.js';
|
|
4
|
+
import { getGlobalConfigPath, getGlobalConfigDir, DEFAULT_CONFIG } from '../constants.js';
|
|
5
|
+
import logger from '../logger/logger.js';
|
|
6
6
|
/**
|
|
7
7
|
* Configuration change event types
|
|
8
8
|
*/
|
|
@@ -20,22 +20,44 @@ export class ConfigManager extends EventEmitter {
|
|
|
20
20
|
configFilePath;
|
|
21
21
|
/**
|
|
22
22
|
* Private constructor to enforce singleton pattern
|
|
23
|
+
* @param configFilePath - Optional path to the config file. If not provided, uses global config path
|
|
23
24
|
*/
|
|
24
|
-
constructor() {
|
|
25
|
+
constructor(configFilePath) {
|
|
25
26
|
super();
|
|
26
|
-
this.configFilePath =
|
|
27
|
+
this.configFilePath = configFilePath || getGlobalConfigPath();
|
|
28
|
+
this.ensureConfigExists();
|
|
27
29
|
this.loadConfig();
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
|
-
* Get the singleton instance of
|
|
31
|
-
* @
|
|
32
|
+
* Get the singleton instance of ConfigManager
|
|
33
|
+
* @param configFilePath - Optional path to the config file
|
|
32
34
|
*/
|
|
33
|
-
static getInstance() {
|
|
35
|
+
static getInstance(configFilePath) {
|
|
34
36
|
if (!ConfigManager.instance) {
|
|
35
|
-
ConfigManager.instance = new ConfigManager();
|
|
37
|
+
ConfigManager.instance = new ConfigManager(configFilePath);
|
|
36
38
|
}
|
|
37
39
|
return ConfigManager.instance;
|
|
38
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Ensure the config directory and file exist
|
|
43
|
+
*/
|
|
44
|
+
ensureConfigExists() {
|
|
45
|
+
try {
|
|
46
|
+
const configDir = getGlobalConfigDir();
|
|
47
|
+
if (!fs.existsSync(configDir)) {
|
|
48
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
49
|
+
logger.info(`Created config directory: ${configDir}`);
|
|
50
|
+
}
|
|
51
|
+
if (!fs.existsSync(this.configFilePath)) {
|
|
52
|
+
fs.writeFileSync(this.configFilePath, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
53
|
+
logger.info(`Created default config file: ${this.configFilePath}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
logger.error(`Failed to ensure config exists: ${error}`);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
39
61
|
/**
|
|
40
62
|
* Load the configuration from the config file
|
|
41
63
|
*/
|
|
@@ -1,38 +1,74 @@
|
|
|
1
1
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
3
|
+
import { z, ZodError } from 'zod';
|
|
4
|
+
import logger from '../logger/logger.js';
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @returns Record of transport configurations
|
|
6
|
+
* Zod schema for transport configuration
|
|
7
7
|
*/
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
export const transportConfigSchema = z.object({
|
|
9
|
+
type: z.enum(['stdio', 'sse', 'http']).optional(),
|
|
10
|
+
disabled: z.boolean().optional(),
|
|
11
|
+
// SSEServerParameters fields
|
|
12
|
+
url: z.string().url().optional(),
|
|
13
|
+
headers: z.record(z.string()).optional(),
|
|
14
|
+
// StdioServerParameters fields
|
|
15
|
+
command: z.string().optional(),
|
|
16
|
+
args: z.array(z.string()).optional(),
|
|
17
|
+
stderr: z.union([z.string(), z.number()]).optional(),
|
|
18
|
+
cwd: z.string().optional(),
|
|
19
|
+
env: z.record(z.string()).optional(),
|
|
20
|
+
});
|
|
11
21
|
/**
|
|
12
22
|
* Creates transport instances from configuration
|
|
13
23
|
* @returns Record of transport instances
|
|
14
24
|
*/
|
|
15
|
-
export function createTransports() {
|
|
25
|
+
export function createTransports(mcpConfig) {
|
|
16
26
|
const transports = {};
|
|
17
|
-
const mcpConfig = loadTransportConfig();
|
|
18
27
|
for (const [name, transport] of Object.entries(mcpConfig)) {
|
|
19
28
|
if (transport.disabled) {
|
|
20
29
|
logger.debug(`Skipping disabled transport: ${name}`);
|
|
21
30
|
continue;
|
|
22
31
|
}
|
|
23
32
|
try {
|
|
33
|
+
// Validate transport configuration
|
|
34
|
+
const validatedTransport = transportConfigSchema.parse(transport);
|
|
24
35
|
// Merge environment variables
|
|
25
|
-
|
|
36
|
+
validatedTransport.env = {
|
|
26
37
|
...Object.fromEntries(Object.entries(process.env)
|
|
27
38
|
.filter(([_, v]) => v !== undefined)
|
|
28
39
|
.map(([k, v]) => [k, String(v)])),
|
|
29
|
-
...
|
|
40
|
+
...validatedTransport.env,
|
|
30
41
|
};
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
// Create transport based on type
|
|
43
|
+
if (validatedTransport.type === 'sse' || validatedTransport.type === 'http') {
|
|
44
|
+
if (!validatedTransport.url) {
|
|
45
|
+
throw new Error(`URL is required for SSE transport: ${name}`);
|
|
46
|
+
}
|
|
47
|
+
transports[name] = new SSEClientTransport(new URL(validatedTransport.url), {
|
|
48
|
+
requestInit: {
|
|
49
|
+
headers: validatedTransport.headers,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// For stdio transport, ensure required fields are present
|
|
55
|
+
if (!validatedTransport.command) {
|
|
56
|
+
throw new Error(`Command is required for stdio transport: ${name}`);
|
|
57
|
+
}
|
|
58
|
+
transports[name] = new StdioClientTransport(validatedTransport);
|
|
59
|
+
}
|
|
60
|
+
logger.debug(`Created ${validatedTransport.type} transport for ${name}`);
|
|
33
61
|
}
|
|
34
62
|
catch (error) {
|
|
35
|
-
|
|
63
|
+
if (error instanceof ZodError) {
|
|
64
|
+
logger.error(`Invalid transport configuration for ${name}:`, error.errors);
|
|
65
|
+
}
|
|
66
|
+
else if (error instanceof Error) {
|
|
67
|
+
logger.error(`Failed to create transport for ${name}: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
logger.error(`Failed to create transport for ${name}: ${String(error)}`);
|
|
71
|
+
}
|
|
36
72
|
}
|
|
37
73
|
}
|
|
38
74
|
return transports;
|
package/build/constants.js
CHANGED
|
@@ -22,10 +22,40 @@ export const ERROR_CODES = {
|
|
|
22
22
|
// MCP constants
|
|
23
23
|
export const MCP_CONFIG_FILE = 'mcp.json';
|
|
24
24
|
export const MCP_SERVER_NAME = '1mcp';
|
|
25
|
-
export const MCP_SERVER_VERSION = '0.
|
|
25
|
+
export const MCP_SERVER_VERSION = '0.2.0';
|
|
26
26
|
export const MCP_URI_SEPARATOR = '_1mcp_';
|
|
27
|
+
// Global config paths
|
|
28
|
+
export const CONFIG_DIR_NAME = '1mcp';
|
|
29
|
+
export const DEFAULT_CONFIG = {
|
|
30
|
+
mcpServers: {},
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Get the global config directory path based on OS
|
|
34
|
+
*/
|
|
35
|
+
export function getGlobalConfigDir() {
|
|
36
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
37
|
+
if (!homeDir) {
|
|
38
|
+
throw new Error('Could not determine home directory');
|
|
39
|
+
}
|
|
40
|
+
const configDir = process.platform === 'darwin' || process.platform === 'linux'
|
|
41
|
+
? `${homeDir}/.config/${CONFIG_DIR_NAME}`
|
|
42
|
+
: `${homeDir}/AppData/Roaming/${CONFIG_DIR_NAME}`;
|
|
43
|
+
return configDir;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get the global config file path
|
|
47
|
+
*/
|
|
48
|
+
export function getGlobalConfigPath() {
|
|
49
|
+
return `${getGlobalConfigDir()}/${MCP_CONFIG_FILE}`;
|
|
50
|
+
}
|
|
27
51
|
// Connection retry settings
|
|
28
52
|
export const CONNECTION_RETRY = {
|
|
29
53
|
MAX_ATTEMPTS: 3,
|
|
30
54
|
INITIAL_DELAY_MS: 1000,
|
|
31
55
|
};
|
|
56
|
+
export const MCP_SERVER_CAPABILITIES = {
|
|
57
|
+
logging: {},
|
|
58
|
+
resources: {},
|
|
59
|
+
tools: {},
|
|
60
|
+
prompts: {},
|
|
61
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CancelledNotificationSchema, ProgressNotificationSchema, LoggingMessageNotificationSchema, ResourceUpdatedNotificationSchema, ResourceListChangedNotificationSchema, ToolListChangedNotificationSchema, PromptListChangedNotificationSchema, InitializedNotificationSchema, RootsListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import logger from '../logger.js';
|
|
2
|
+
import logger from '../logger/logger.js';
|
|
3
3
|
import { withErrorHandling } from '../utils/errorHandling.js';
|
|
4
4
|
/**
|
|
5
5
|
* Sets up client-to-server notification handlers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourceTemplatesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import logger from '../logger.js';
|
|
3
|
-
import { setLogLevel } from '../logger.js';
|
|
2
|
+
import logger from '../logger/logger.js';
|
|
3
|
+
import { setLogLevel } from '../logger/logger.js';
|
|
4
4
|
import { MCP_URI_SEPARATOR, MCP_SERVER_NAME } from '../constants.js';
|
|
5
5
|
import { executeClientOperation } from '../clients/clientManager.js';
|
|
6
6
|
import { ProxyError, parseUri, withErrorHandling } from '../utils/errorHandling.js';
|
package/build/index.js
CHANGED
|
@@ -1,115 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import express from 'express';
|
|
3
2
|
import yargs from 'yargs';
|
|
4
3
|
import { hideBin } from 'yargs/helpers';
|
|
5
|
-
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
6
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
-
import {
|
|
8
|
-
import logger
|
|
9
|
-
import { PORT, SSE_ENDPOINT, MESSAGES_ENDPOINT, ERROR_CODES } from './constants.js';
|
|
5
|
+
import { setupServer } from './server.js';
|
|
6
|
+
import logger from './logger/logger.js';
|
|
10
7
|
import configReloadService from './services/configReloadService.js';
|
|
8
|
+
import { ConfigManager } from './config/configManager.js';
|
|
9
|
+
import { ExpressServer } from './server/expressServer.js';
|
|
11
10
|
// Parse command line arguments
|
|
12
11
|
const argv = yargs(hideBin(process.argv))
|
|
13
12
|
.usage('Usage: $0 [options]')
|
|
14
13
|
.options({
|
|
15
14
|
transport: {
|
|
16
15
|
alias: 't',
|
|
17
|
-
describe: 'Transport type to use (stdio or
|
|
16
|
+
describe: 'Transport type to use (stdio or sse)',
|
|
18
17
|
type: 'string',
|
|
19
|
-
choices: ['stdio', '
|
|
20
|
-
default: '
|
|
18
|
+
choices: ['stdio', 'sse'],
|
|
19
|
+
default: 'sse',
|
|
20
|
+
},
|
|
21
|
+
config: {
|
|
22
|
+
alias: 'c',
|
|
23
|
+
describe: 'Path to the config file',
|
|
24
|
+
type: 'string',
|
|
25
|
+
default: undefined,
|
|
21
26
|
},
|
|
22
27
|
})
|
|
23
28
|
.help()
|
|
24
29
|
.alias('help', 'h')
|
|
25
30
|
.parseSync();
|
|
26
|
-
const app = express();
|
|
27
|
-
// app.use(express.json());
|
|
28
|
-
// app.use(express.urlencoded({ extended: true }));
|
|
29
|
-
const transportMap = new Map();
|
|
30
|
-
// Add error handling middleware
|
|
31
|
-
app.use((err, req, res, next) => {
|
|
32
|
-
logger.error('Express error:', err);
|
|
33
|
-
res.status(500).json({
|
|
34
|
-
error: {
|
|
35
|
-
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
36
|
-
message: 'Internal server error',
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
app.get(SSE_ENDPOINT, async (req, res) => {
|
|
41
|
-
try {
|
|
42
|
-
logger.info('sse', { query: req.query, headers: req.headers });
|
|
43
|
-
const transport = new SSEServerTransport(MESSAGES_ENDPOINT, res);
|
|
44
|
-
await server.connect(transport);
|
|
45
|
-
transportMap.set(transport.sessionId, transport);
|
|
46
|
-
// Update MCP transport connection status when a client connects
|
|
47
|
-
if (transportMap.size === 1) {
|
|
48
|
-
setMCPTransportConnected(true);
|
|
49
|
-
logger.info('First client connected, enabling MCP logging transport');
|
|
50
|
-
}
|
|
51
|
-
transport.onclose = () => {
|
|
52
|
-
transportMap.delete(transport.sessionId);
|
|
53
|
-
logger.info('transport closed', transport.sessionId);
|
|
54
|
-
// Update MCP transport connection status when all clients disconnect
|
|
55
|
-
if (transportMap.size === 0) {
|
|
56
|
-
setMCPTransportConnected(false);
|
|
57
|
-
logger.info('All clients disconnected, disabling MCP logging transport');
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
logger.error('SSE connection error:', error);
|
|
63
|
-
res.status(500).end();
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
app.post(MESSAGES_ENDPOINT, async (req, res) => {
|
|
67
|
-
try {
|
|
68
|
-
const sessionId = req.query.sessionId;
|
|
69
|
-
if (!sessionId) {
|
|
70
|
-
res.status(400).json({
|
|
71
|
-
error: {
|
|
72
|
-
code: ERROR_CODES.INVALID_PARAMS,
|
|
73
|
-
message: 'Invalid params: sessionId is required',
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
logger.info('message', { body: req.body, sessionId });
|
|
79
|
-
const transport = transportMap.get(sessionId);
|
|
80
|
-
if (transport) {
|
|
81
|
-
await transport.handlePostMessage(req, res);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
res.status(404).json({
|
|
85
|
-
error: {
|
|
86
|
-
code: ERROR_CODES.TRANSPORT_NOT_FOUND,
|
|
87
|
-
message: 'Transport not found',
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
logger.error('Message handling error:', error);
|
|
93
|
-
res.status(500).json({
|
|
94
|
-
error: {
|
|
95
|
-
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
96
|
-
message: 'Internal server error',
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
31
|
/**
|
|
102
32
|
* Set up graceful shutdown handling
|
|
103
33
|
*/
|
|
104
|
-
function setupGracefulShutdown() {
|
|
34
|
+
function setupGracefulShutdown(serverManager) {
|
|
105
35
|
const shutdown = async () => {
|
|
106
36
|
logger.info('Shutting down server...');
|
|
107
37
|
// Stop the configuration reload service
|
|
108
38
|
configReloadService.stop();
|
|
109
39
|
// Close all transports
|
|
110
|
-
for (const [sessionId, transport] of
|
|
40
|
+
for (const [sessionId, transport] of serverManager.getTransports().entries()) {
|
|
111
41
|
try {
|
|
112
|
-
transport
|
|
42
|
+
transport?.close();
|
|
113
43
|
logger.info(`Closed transport: ${sessionId}`);
|
|
114
44
|
}
|
|
115
45
|
catch (error) {
|
|
@@ -128,19 +58,27 @@ function setupGracefulShutdown() {
|
|
|
128
58
|
* Start the server using the specified transport.
|
|
129
59
|
*/
|
|
130
60
|
async function main() {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
61
|
+
try {
|
|
62
|
+
ConfigManager.getInstance(argv.config);
|
|
63
|
+
// Initialize server and get server manager with custom config path if provided
|
|
64
|
+
const serverManager = await setupServer();
|
|
65
|
+
// Set up graceful shutdown handling
|
|
66
|
+
setupGracefulShutdown(serverManager);
|
|
67
|
+
if (argv.transport === 'stdio') {
|
|
68
|
+
// Use stdio transport
|
|
69
|
+
const transport = new StdioServerTransport();
|
|
70
|
+
await serverManager.connectTransport(transport, 'stdio');
|
|
71
|
+
logger.info('Server started with stdio transport');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Use HTTP/SSE transport
|
|
75
|
+
const expressServer = new ExpressServer(serverManager);
|
|
76
|
+
expressServer.start();
|
|
77
|
+
}
|
|
138
78
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
logger.info(`Server is running on port ${PORT} with HTTP/SSE transport`);
|
|
143
|
-
});
|
|
79
|
+
catch (error) {
|
|
80
|
+
logger.error('Server error:', error);
|
|
81
|
+
process.exit(1);
|
|
144
82
|
}
|
|
145
83
|
}
|
|
146
84
|
main().catch((error) => {
|
|
@@ -23,14 +23,18 @@ const customFormat = winston.format.combine(winston.format.timestamp(), winston.
|
|
|
23
23
|
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
|
|
24
24
|
return `${timestamp} [${level.toUpperCase()}] ${message}${metaStr}`;
|
|
25
25
|
}));
|
|
26
|
+
const consoleFormat = winston.format.combine(winston.format.timestamp(), winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
27
|
+
const keys = Object.keys(meta);
|
|
28
|
+
const metaStr = keys.length > 0 ? ` ${keys.map((key) => `${key}=${JSON.stringify(meta[key])}`).join(' ')}` : '';
|
|
29
|
+
return `${timestamp} [${level.toUpperCase()}] message=${JSON.stringify(message)}${metaStr}`;
|
|
30
|
+
}));
|
|
26
31
|
// Create the logger without the MCP transport initially
|
|
27
32
|
const logger = winston.createLogger({
|
|
28
33
|
level: 'info',
|
|
29
34
|
format: customFormat,
|
|
30
|
-
defaultMeta: { service: '1mcp' },
|
|
31
35
|
transports: [
|
|
32
36
|
new winston.transports.Console({
|
|
33
|
-
format:
|
|
37
|
+
format: consoleFormat,
|
|
34
38
|
}),
|
|
35
39
|
//
|
|
36
40
|
// - Write all logs with importance level of `error` or higher to `error.log`
|
|
@@ -64,7 +68,7 @@ export function addMCPTransport(server, loggerName) {
|
|
|
64
68
|
// Add the MCP transport to the logger
|
|
65
69
|
mcpTransport = new MCPTransport({
|
|
66
70
|
server,
|
|
67
|
-
loggerName: loggerName || '1mcp
|
|
71
|
+
loggerName: loggerName || '1mcp',
|
|
68
72
|
level: 'info',
|
|
69
73
|
});
|
|
70
74
|
logger.add(mcpTransport);
|
|
@@ -18,14 +18,30 @@ export class MCPTransport extends Transport {
|
|
|
18
18
|
constructor(options) {
|
|
19
19
|
super(options);
|
|
20
20
|
this.server = options.server;
|
|
21
|
-
this.loggerName = options.loggerName || '1mcp
|
|
21
|
+
this.loggerName = options.loggerName || '1mcp';
|
|
22
|
+
// Set up connection status handlers
|
|
23
|
+
this.setupConnectionHandlers();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Set up handlers for connection status changes
|
|
27
|
+
*/
|
|
28
|
+
setupConnectionHandlers() {
|
|
29
|
+
// Handle connection close
|
|
30
|
+
this.server.onclose = () => {
|
|
31
|
+
this.setConnected(false);
|
|
32
|
+
};
|
|
22
33
|
}
|
|
23
34
|
/**
|
|
24
35
|
* Set the connection status of the transport
|
|
25
36
|
* @param connected Whether the server is connected
|
|
26
37
|
*/
|
|
27
38
|
setConnected(connected) {
|
|
39
|
+
const previousState = this.connected;
|
|
28
40
|
this.connected = connected;
|
|
41
|
+
// Log connection state changes
|
|
42
|
+
if (previousState !== connected) {
|
|
43
|
+
console.debug(`MCP Transport connection state changed to: ${connected ? 'connected' : 'disconnected'}`);
|
|
44
|
+
}
|
|
29
45
|
}
|
|
30
46
|
/**
|
|
31
47
|
* Send a log notification to the MCP client
|
|
@@ -112,11 +128,23 @@ export class MCPTransport extends Transport {
|
|
|
112
128
|
setImmediate(() => {
|
|
113
129
|
this.emit('logged', info);
|
|
114
130
|
});
|
|
115
|
-
if
|
|
116
|
-
|
|
117
|
-
|
|
131
|
+
// Only try to send if connected
|
|
132
|
+
if (this.connected) {
|
|
133
|
+
try {
|
|
134
|
+
this.sendLogNotification(info);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
// Ignore any errors during log sending
|
|
138
|
+
}
|
|
118
139
|
}
|
|
119
|
-
|
|
140
|
+
// Always call the callback to prevent blocking
|
|
120
141
|
callback();
|
|
121
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Clean up resources when the transport is closed
|
|
145
|
+
*/
|
|
146
|
+
close() {
|
|
147
|
+
this.setConnected(false);
|
|
148
|
+
this.server.onclose = undefined;
|
|
149
|
+
}
|
|
122
150
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import logger from '../logger/logger.js';
|
|
3
|
+
const activeRequests = new Map();
|
|
4
|
+
/**
|
|
5
|
+
* Logs MCP request details
|
|
6
|
+
*/
|
|
7
|
+
function logRequest(requestId, method, params) {
|
|
8
|
+
logger.info('MCP Request', {
|
|
9
|
+
requestId,
|
|
10
|
+
method,
|
|
11
|
+
params: JSON.stringify(params),
|
|
12
|
+
timestamp: new Date().toISOString(),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Logs MCP response details
|
|
17
|
+
*/
|
|
18
|
+
function logResponse(requestId, result, duration) {
|
|
19
|
+
logger.info('MCP Response', {
|
|
20
|
+
requestId,
|
|
21
|
+
duration,
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Logs MCP error details
|
|
27
|
+
*/
|
|
28
|
+
function logError(requestId, error, duration) {
|
|
29
|
+
logger.error('MCP Error', {
|
|
30
|
+
requestId,
|
|
31
|
+
error: error instanceof Error ? error.message : JSON.stringify(error),
|
|
32
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
33
|
+
duration,
|
|
34
|
+
timestamp: new Date().toISOString(),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Logs MCP notification details
|
|
39
|
+
*/
|
|
40
|
+
function logNotification(method, params) {
|
|
41
|
+
logger.info('MCP Notification', {
|
|
42
|
+
method,
|
|
43
|
+
params: JSON.stringify(params),
|
|
44
|
+
timestamp: new Date().toISOString(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Wraps the original request handler with logging
|
|
49
|
+
*/
|
|
50
|
+
function wrapRequestHandler(originalHandler, method) {
|
|
51
|
+
return async (request, extra) => {
|
|
52
|
+
const requestId = uuidv4();
|
|
53
|
+
const startTime = Date.now();
|
|
54
|
+
// Store request context
|
|
55
|
+
activeRequests.set(requestId, {
|
|
56
|
+
requestId,
|
|
57
|
+
method,
|
|
58
|
+
startTime,
|
|
59
|
+
});
|
|
60
|
+
// Log request
|
|
61
|
+
logRequest(requestId, method, request.params);
|
|
62
|
+
try {
|
|
63
|
+
// Execute original handler
|
|
64
|
+
const result = await originalHandler(request, extra);
|
|
65
|
+
// Log response
|
|
66
|
+
const duration = Date.now() - startTime;
|
|
67
|
+
logResponse(requestId, result, duration);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
// Log error
|
|
72
|
+
const duration = Date.now() - startTime;
|
|
73
|
+
logError(requestId, error, duration);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
// Clean up request context
|
|
78
|
+
activeRequests.delete(requestId);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Wraps the original notification handler with logging
|
|
84
|
+
*/
|
|
85
|
+
function wrapNotificationHandler(originalHandler, method) {
|
|
86
|
+
return async (notification) => {
|
|
87
|
+
// Log notification
|
|
88
|
+
logNotification(method, notification.params);
|
|
89
|
+
// Execute original handler
|
|
90
|
+
await originalHandler(notification);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Enhances an MCP server with request/response logging
|
|
95
|
+
*/
|
|
96
|
+
export function enhanceServerWithLogging(server) {
|
|
97
|
+
// Store original methods
|
|
98
|
+
const originalSetRequestHandler = server.setRequestHandler.bind(server);
|
|
99
|
+
const originalSetNotificationHandler = server.setNotificationHandler.bind(server);
|
|
100
|
+
const originalNotification = server.notification.bind(server);
|
|
101
|
+
// Override request handler registration
|
|
102
|
+
server.setRequestHandler = (schema, handler) => {
|
|
103
|
+
return originalSetRequestHandler(schema, wrapRequestHandler(handler, schema._def.shape().method._def.value));
|
|
104
|
+
};
|
|
105
|
+
// Override notification handler registration
|
|
106
|
+
server.setNotificationHandler = (schema, handler) => {
|
|
107
|
+
return originalSetNotificationHandler(schema, wrapNotificationHandler(handler, schema._def.shape().method._def.value));
|
|
108
|
+
};
|
|
109
|
+
// Override notification sending
|
|
110
|
+
server.notification = (notification) => {
|
|
111
|
+
logNotification(notification.method, notification.params);
|
|
112
|
+
return originalNotification(notification);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
3
|
+
import logger, { setMCPTransportConnected } from '../logger/logger.js';
|
|
4
|
+
import { PORT, SSE_ENDPOINT, MESSAGES_ENDPOINT, ERROR_CODES } from '../constants.js';
|
|
5
|
+
export class ExpressServer {
|
|
6
|
+
app;
|
|
7
|
+
serverManager;
|
|
8
|
+
constructor(serverManager) {
|
|
9
|
+
this.app = express();
|
|
10
|
+
this.serverManager = serverManager;
|
|
11
|
+
this.setupMiddleware();
|
|
12
|
+
this.setupRoutes();
|
|
13
|
+
}
|
|
14
|
+
setupMiddleware() {
|
|
15
|
+
// Add error handling middleware
|
|
16
|
+
this.app.use((err, req, res, next) => {
|
|
17
|
+
logger.error('Express error:', err);
|
|
18
|
+
res.status(500).json({
|
|
19
|
+
error: {
|
|
20
|
+
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
21
|
+
message: 'Internal server error',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
setupRoutes() {
|
|
27
|
+
this.app.get(SSE_ENDPOINT, async (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
logger.info('sse', { query: req.query, headers: req.headers });
|
|
30
|
+
const transport = new SSEServerTransport(MESSAGES_ENDPOINT, res);
|
|
31
|
+
// Connect the transport using the server manager
|
|
32
|
+
await this.serverManager.connectTransport(transport, transport.sessionId);
|
|
33
|
+
// Update MCP transport connection status when a client connects
|
|
34
|
+
if (this.serverManager.getActiveTransportsCount() === 1) {
|
|
35
|
+
setMCPTransportConnected(true);
|
|
36
|
+
logger.info('First client connected, enabling MCP logging transport');
|
|
37
|
+
}
|
|
38
|
+
transport.onclose = () => {
|
|
39
|
+
this.serverManager.disconnectTransport(transport.sessionId);
|
|
40
|
+
logger.info('transport closed', transport.sessionId);
|
|
41
|
+
// Update MCP transport connection status when all clients disconnect
|
|
42
|
+
if (this.serverManager.getActiveTransportsCount() === 0) {
|
|
43
|
+
setMCPTransportConnected(false);
|
|
44
|
+
logger.info('All clients disconnected, disabling MCP logging transport');
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
logger.error('SSE connection error:', error);
|
|
50
|
+
res.status(500).end();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
this.app.post(MESSAGES_ENDPOINT, async (req, res) => {
|
|
54
|
+
try {
|
|
55
|
+
const sessionId = req.query.sessionId;
|
|
56
|
+
if (!sessionId) {
|
|
57
|
+
res.status(400).json({
|
|
58
|
+
error: {
|
|
59
|
+
code: ERROR_CODES.INVALID_PARAMS,
|
|
60
|
+
message: 'Invalid params: sessionId is required',
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
logger.info('message', { body: req.body, sessionId });
|
|
66
|
+
const transport = this.serverManager.getTransport(sessionId);
|
|
67
|
+
if (transport instanceof SSEServerTransport) {
|
|
68
|
+
await transport.handlePostMessage(req, res);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
res.status(404).json({
|
|
72
|
+
error: {
|
|
73
|
+
code: ERROR_CODES.TRANSPORT_NOT_FOUND,
|
|
74
|
+
message: 'Transport not found',
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
logger.error('Message handling error:', error);
|
|
80
|
+
res.status(500).json({
|
|
81
|
+
error: {
|
|
82
|
+
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
83
|
+
message: 'Internal server error',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
start() {
|
|
90
|
+
this.app.listen(PORT, () => {
|
|
91
|
+
logger.info(`Server is running on port ${PORT} with HTTP/SSE transport`);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
package/build/server.js
CHANGED
|
@@ -1,61 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import logger, { addMCPTransport } from './logger.js';
|
|
1
|
+
import { MCP_SERVER_CAPABILITIES, MCP_SERVER_NAME, MCP_SERVER_VERSION } from './constants.js';
|
|
2
|
+
import logger from './logger/logger.js';
|
|
4
3
|
import { createTransports } from './config/transportConfig.js';
|
|
5
4
|
import { createClients } from './clients/clientManager.js';
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
/**
|
|
9
|
-
* Initialize and configure the MCP server
|
|
10
|
-
* @returns The configured MCP server instance
|
|
11
|
-
*/
|
|
12
|
-
async function initializeServer() {
|
|
13
|
-
try {
|
|
14
|
-
// Create the server instance
|
|
15
|
-
const server = new Server({
|
|
16
|
-
name: MCP_SERVER_NAME,
|
|
17
|
-
version: MCP_SERVER_VERSION,
|
|
18
|
-
}, {
|
|
19
|
-
capabilities: {
|
|
20
|
-
logging: {},
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
// Initialize the MCP transport for logging
|
|
24
|
-
addMCPTransport(server, MCP_SERVER_NAME);
|
|
25
|
-
logger.info('Server created and MCP transport initialized for logging');
|
|
26
|
-
return server;
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
logger.error(`Failed to initialize server: ${error}`);
|
|
30
|
-
throw error;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
5
|
+
import { ServerManager } from './serverManager.js';
|
|
6
|
+
import { ConfigManager } from './config/configManager.js';
|
|
33
7
|
/**
|
|
34
8
|
* Main function to set up the MCP server
|
|
35
9
|
*/
|
|
36
10
|
async function setupServer() {
|
|
37
11
|
try {
|
|
38
|
-
|
|
39
|
-
const server = await initializeServer();
|
|
12
|
+
const mcpConfig = ConfigManager.getInstance().getTransportConfig();
|
|
40
13
|
// Create transports from configuration
|
|
41
|
-
const transports = createTransports();
|
|
14
|
+
const transports = createTransports(mcpConfig);
|
|
42
15
|
logger.info(`Created ${Object.keys(transports).length} transports`);
|
|
43
16
|
// Create clients for each transport
|
|
44
17
|
const clients = await createClients(transports);
|
|
45
18
|
logger.info(`Created ${Object.keys(clients).length} clients`);
|
|
46
|
-
|
|
47
|
-
await collectAndRegisterCapabilities(clients, server);
|
|
48
|
-
// Initialize the configuration reload service
|
|
49
|
-
configReloadService.initialize(server, transports);
|
|
50
|
-
logger.info('Configuration reload service initialized');
|
|
19
|
+
const serverManager = ServerManager.getInstance({ name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION }, { capabilities: MCP_SERVER_CAPABILITIES }, clients, transports);
|
|
51
20
|
logger.info('Server setup completed successfully');
|
|
52
|
-
return
|
|
21
|
+
return serverManager;
|
|
53
22
|
}
|
|
54
23
|
catch (error) {
|
|
55
24
|
logger.error(`Failed to set up server: ${error}`);
|
|
56
25
|
throw error;
|
|
57
26
|
}
|
|
58
27
|
}
|
|
59
|
-
|
|
60
|
-
const server = await setupServer();
|
|
61
|
-
export { server };
|
|
28
|
+
export { setupServer };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import logger from './logger/logger.js';
|
|
3
|
+
import configReloadService from './services/configReloadService.js';
|
|
4
|
+
import { setupCapabilities } from './capabilities/capabilityManager.js';
|
|
5
|
+
import { enhanceServerWithLogging } from './middleware/loggingMiddleware.js';
|
|
6
|
+
export class ServerManager {
|
|
7
|
+
static instance;
|
|
8
|
+
servers = new Map();
|
|
9
|
+
serverConfig;
|
|
10
|
+
serverCapabilities;
|
|
11
|
+
clients = {};
|
|
12
|
+
clientTransports = {};
|
|
13
|
+
constructor(config, capabilities, clients, clientTransports) {
|
|
14
|
+
this.serverConfig = config;
|
|
15
|
+
this.serverCapabilities = capabilities;
|
|
16
|
+
this.clients = clients;
|
|
17
|
+
this.clientTransports = clientTransports;
|
|
18
|
+
}
|
|
19
|
+
static getInstance(config, capabilities, clients, clientTransports) {
|
|
20
|
+
if (!ServerManager.instance) {
|
|
21
|
+
ServerManager.instance = new ServerManager(config, capabilities, clients, clientTransports);
|
|
22
|
+
}
|
|
23
|
+
return ServerManager.instance;
|
|
24
|
+
}
|
|
25
|
+
async connectTransport(transport, sessionId) {
|
|
26
|
+
try {
|
|
27
|
+
// Create a new server instance for this transport
|
|
28
|
+
const server = new Server(this.serverConfig, this.serverCapabilities);
|
|
29
|
+
// Enhance server with logging middleware
|
|
30
|
+
enhanceServerWithLogging(server);
|
|
31
|
+
// Collect capabilities and register handlers
|
|
32
|
+
await setupCapabilities(this.clients, server);
|
|
33
|
+
// Initialize the configuration reload service
|
|
34
|
+
configReloadService.initialize(server, this.clientTransports);
|
|
35
|
+
// Connect the transport to the new server instance
|
|
36
|
+
await server.connect(transport);
|
|
37
|
+
// Store the server instance
|
|
38
|
+
this.servers.set(sessionId, server);
|
|
39
|
+
logger.info(`Connected transport for session ${sessionId}`);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
logger.error(`Failed to connect transport for session ${sessionId}:`, error);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
disconnectTransport(sessionId) {
|
|
47
|
+
const server = this.servers.get(sessionId);
|
|
48
|
+
if (server) {
|
|
49
|
+
this.servers.delete(sessionId);
|
|
50
|
+
logger.info(`Disconnected transport for session ${sessionId}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
getTransport(sessionId) {
|
|
54
|
+
return this.servers.get(sessionId)?.transport;
|
|
55
|
+
}
|
|
56
|
+
getTransports() {
|
|
57
|
+
const transports = new Map();
|
|
58
|
+
for (const [id, server] of this.servers.entries()) {
|
|
59
|
+
if (server.transport) {
|
|
60
|
+
transports.set(id, server.transport);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return transports;
|
|
64
|
+
}
|
|
65
|
+
getActiveTransportsCount() {
|
|
66
|
+
return this.servers.size;
|
|
67
|
+
}
|
|
68
|
+
getServer(sessionId) {
|
|
69
|
+
return this.servers.get(sessionId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import logger from '../logger.js';
|
|
1
|
+
import logger from '../logger/logger.js';
|
|
2
2
|
import configManager, { ConfigChangeEvent } from '../config/configManager.js';
|
|
3
3
|
import { createTransports } from '../config/transportConfig.js';
|
|
4
4
|
import { createClients } from '../clients/clientManager.js';
|
|
5
|
-
import {
|
|
5
|
+
import { setupCapabilities } from '../capabilities/capabilityManager.js';
|
|
6
6
|
/**
|
|
7
7
|
* Service to handle dynamic configuration reloading
|
|
8
8
|
*/
|
|
@@ -49,32 +49,25 @@ export class ConfigReloadService {
|
|
|
49
49
|
}
|
|
50
50
|
this.isReloading = true;
|
|
51
51
|
logger.info('Handling configuration change...');
|
|
52
|
+
// Store current transports for reconnection
|
|
53
|
+
const currentTransportEntries = Object.entries(this.currentTransports);
|
|
52
54
|
try {
|
|
53
|
-
//
|
|
54
|
-
const
|
|
55
|
-
// Disconnect and clean up old transports that are no longer in the config
|
|
56
|
-
// or have changed configuration
|
|
57
|
-
const transportKeysToRemove = Object.keys(this.currentTransports).filter((key) => !newTransports[key] ||
|
|
58
|
-
JSON.stringify(newConfig[key]) !== JSON.stringify(configManager.getTransportConfig()[key]));
|
|
59
|
-
for (const key of transportKeysToRemove) {
|
|
55
|
+
// Close all current transports
|
|
56
|
+
for (const [key, transport] of currentTransportEntries) {
|
|
60
57
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// Note: Transport interface might not have a standard disconnect method,
|
|
64
|
-
// but we can safely try to close it if it's a closeable transport
|
|
65
|
-
if (transport && 'close' in transport && typeof transport.close === 'function') {
|
|
66
|
-
await transport.close();
|
|
67
|
-
logger.info(`Closed transport: ${key}`);
|
|
68
|
-
}
|
|
58
|
+
await transport.close();
|
|
59
|
+
logger.info(`Closed transport: ${key}`);
|
|
69
60
|
}
|
|
70
61
|
catch (error) {
|
|
71
62
|
logger.error(`Error closing transport ${key}: ${error}`);
|
|
72
63
|
}
|
|
73
64
|
}
|
|
65
|
+
// Create new transports from updated configuration
|
|
66
|
+
const newTransports = createTransports(newConfig);
|
|
74
67
|
// Create clients for the new transports
|
|
75
68
|
const newClients = await createClients(newTransports);
|
|
76
|
-
//
|
|
77
|
-
await
|
|
69
|
+
// Register new capabilities with the server
|
|
70
|
+
await setupCapabilities(newClients, this.server);
|
|
78
71
|
// Update current transports
|
|
79
72
|
this.currentTransports = newTransports;
|
|
80
73
|
logger.info('Configuration reload completed successfully');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1mcp/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "One MCP server to aggregate them all - A unified Model Context Protocol server implementation",
|
|
5
5
|
"homepage": "https://1mcp.app",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,14 +31,17 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.7.0",
|
|
33
33
|
"express": "^4.21.2",
|
|
34
|
+
"uuid": "^11.1.0",
|
|
34
35
|
"winston": "^3.17.0",
|
|
35
36
|
"winston-transport": "^4.9.0",
|
|
36
37
|
"yargs": "^17.7.2",
|
|
37
|
-
"yargs-parser": "^21.1.1"
|
|
38
|
+
"yargs-parser": "^21.1.1",
|
|
39
|
+
"zod": "^3.24.2"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@types/express": "^5.0.0",
|
|
41
43
|
"@types/node": "^22.13.10",
|
|
44
|
+
"@types/uuid": "^10.0.0",
|
|
42
45
|
"@types/yargs": "^17.0.33",
|
|
43
46
|
"husky": "^9.1.7",
|
|
44
47
|
"lint-staged": "^15.5.0",
|