@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 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 coding assistants. Instead of configuring multiple MCP servers for different clients (Cursor, Roo Code, Claude, etc.), 1MCP provides a single, unified server that:
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 coding assistants
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
- The implementation includes:
16
+ ## Quick Start
15
17
 
16
- - Express-based HTTP server with SSE (Server-Sent Events) support
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
- ## Server Implementation
22
-
23
- The server is implemented using:
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
- ### Features
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
- - Seamless integration with Winston logger
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", "http", default: "http")
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 collectAndRegisterCapabilities(clients, server) {
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
- export default async function createClient(transport) {
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: '1mcp',
5
- version: '1.0.0',
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(transport);
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
- logger.info(`Successfully connected to ${name}`);
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 { MCP_CONFIG_FILE } from '../constants.js';
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 = path.resolve(process.cwd(), MCP_CONFIG_FILE);
27
+ this.configFilePath = configFilePath || getGlobalConfigPath();
28
+ this.ensureConfigExists();
27
29
  this.loadConfig();
28
30
  }
29
31
  /**
30
- * Get the singleton instance of the ConfigManager
31
- * @returns The ConfigManager instance
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 logger from '../logger.js';
3
- import configManager from './configManager.js';
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
- * Loads transport configurations from the MCP config file
6
- * @returns Record of transport configurations
6
+ * Zod schema for transport configuration
7
7
  */
8
- export function loadTransportConfig() {
9
- return configManager.getTransportConfig();
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
- transport.env = {
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
- ...transport.env,
40
+ ...validatedTransport.env,
30
41
  };
31
- transports[name] = new StdioClientTransport(transport);
32
- logger.debug(`Created transport for ${name}`);
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
- logger.error(`Failed to create transport for ${name}: ${error}`);
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;
@@ -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.1.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 { server } from './server.js';
8
- import logger, { setMCPTransportConnected } from './logger.js';
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 http)',
16
+ describe: 'Transport type to use (stdio or sse)',
18
17
  type: 'string',
19
- choices: ['stdio', 'http'],
20
- default: 'http',
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 transportMap.entries()) {
40
+ for (const [sessionId, transport] of serverManager.getTransports().entries()) {
111
41
  try {
112
- transport.close();
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
- // Set up graceful shutdown handling
132
- setupGracefulShutdown();
133
- if (argv.transport === 'stdio') {
134
- // Use stdio transport
135
- const transport = new StdioServerTransport();
136
- await server.connect(transport);
137
- logger.info('Server started with stdio transport');
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
- else {
140
- // Use HTTP/SSE transport
141
- app.listen(PORT, () => {
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: customFormat,
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-agent',
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-agent';
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 (!this.connected) {
116
- callback();
117
- return;
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
- this.sendLogNotification(info);
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 { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from './constants.js';
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 { collectAndRegisterCapabilities } from './capabilities/capabilityManager.js';
7
- import configReloadService from './services/configReloadService.js';
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
- // Initialize the server
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
- // Collect capabilities and register handlers
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 server;
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
- // Set up the server and export it
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 { collectAndRegisterCapabilities } from '../capabilities/capabilityManager.js';
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
- // Create new transports from updated configuration
54
- const newTransports = createTransports();
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
- const transport = this.currentTransports[key];
62
- // Attempt to close the transport if possible
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
- // Recollect and register capabilities with the server
77
- await collectAndRegisterCapabilities(newClients, this.server);
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');
@@ -1,5 +1,5 @@
1
1
  import { ERROR_CODES } from '../constants.js';
2
- import logger from '../logger.js';
2
+ import logger from '../logger/logger.js';
3
3
  /**
4
4
  * Custom error types for the MCP agent
5
5
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1mcp/agent",
3
- "version": "0.1.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",