@1mcp/agent 0.2.0 → 0.4.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
@@ -28,7 +28,7 @@ npx -y @1mcp/agent --config ~/Library/Application\ Support/Claude/claude_desktop
28
28
  "mcpServers": {
29
29
  "1mcp": {
30
30
  "type": "http",
31
- "url": "http://localhost:3050"
31
+ "url": "http://localhost:3050/sse"
32
32
  }
33
33
  }
34
34
  }
@@ -41,24 +41,81 @@ npx -y @1mcp/agent --config ~/Library/Application\ Support/Claude/claude_desktop
41
41
  You can run the server directly using `npx`:
42
42
 
43
43
  ```bash
44
- # Run with HTTP/SSE transport (default)
44
+ # Basic usage (starts server with SSE transport)
45
45
  npx -y @1mcp/agent
46
46
 
47
- # Run with stdio transport
48
- npx -y @1mcp/agent --transport stdio
47
+ # Use existing Claude Desktop config
48
+ npx -y @1mcp/agent --config ~/Library/Application\ Support/Claude/claude_desktop_config.json
49
49
 
50
- # Run with AI assistant config file
51
- npx -y @1mcp/agent --config /path/to/ai/assistant/config.json
50
+ # Use stdio transport instead of SSE
51
+ npx -y @1mcp/agent --transport stdio
52
52
 
53
- # Show help
53
+ # Show all available options
54
54
  npx -y @1mcp/agent --help
55
55
  ```
56
56
 
57
57
  Available options:
58
- - `--transport, -t`: Transport type to use (choices: "stdio", "sse", default: "sse")
59
- - `--config, -c`: Path to the configuration file
58
+ - `--transport, -t`: Choose transport type ("stdio" or "sse", default: "sse")
59
+ - `--config, -c`: Use a specific config file
60
+ - `--port, -P`: Change SSE port (default: 3050)
61
+ - `--host, -H`: Change SSE host (default: localhost)
62
+ - `--tags, -g`: Filter servers by tags (see Tags section below)
60
63
  - `--help, -h`: Show help
61
64
 
65
+ ### Understanding Tags
66
+
67
+ Tags help you control which MCP servers are available to different clients. Think of tags as labels that describe what each server can do.
68
+
69
+ #### How to Use Tags
70
+
71
+ 1. **In your server config**: Add tags to each server to describe its capabilities
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "web-server": {
76
+ "command": "uvx",
77
+ "args": ["mcp-server-fetch"],
78
+ "tags": ["network", "web"],
79
+ "disabled": false
80
+ },
81
+ "file-server": {
82
+ "command": "npx",
83
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "~/Downloads"],
84
+ "tags": ["filesystem"],
85
+ "disabled": false
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ 2. **When starting 1MCP in stdio mode**: You can filter servers by tags
92
+ ```bash
93
+ # Only start servers with the "network" tag
94
+ npx -y @1mcp/agent --transport stdio --tags "network"
95
+
96
+ # Start servers with either "network" or "filesystem" tags
97
+ npx -y @1mcp/agent --transport stdio --tags "network,filesystem"
98
+ ```
99
+
100
+ 3. **When using SSE transport**: Clients can request servers with specific tags
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "1mcp": {
105
+ "type": "http",
106
+ "url": "http://localhost:3050/sse?tags=network" // Only connect to network-capable servers
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ Example tags:
113
+ - `network`: For servers that make web requests
114
+ - `filesystem`: For servers that handle file operations
115
+ - `memory`: For servers that provide memory/storage
116
+ - `shell`: For servers that run shell commands
117
+ - `db`: For servers that handle database operations
118
+
62
119
  ## Configuration
63
120
 
64
121
  ### Global Configuration
@@ -5,16 +5,17 @@ import { registerRequestHandlers } from '../handlers/requestHandlers.js';
5
5
  * Collects capabilities from all clients and registers them with the server
6
6
  * @param clients Record of client instances
7
7
  * @param server The MCP server instance
8
+ * @param tags Array of tags to filter clients by
8
9
  * @returns The combined server capabilities
9
10
  */
10
- export async function setupCapabilities(clients, server) {
11
+ export async function setupCapabilities(clients, serverInfo) {
11
12
  // Collect capabilities from all clients
12
13
  const capabilities = collectCapabilities(clients);
13
14
  // Set up notification handlers
14
- setupClientToServerNotifications(clients, server);
15
- setupServerToClientNotifications(clients, server);
15
+ setupClientToServerNotifications(clients, serverInfo);
16
+ setupServerToClientNotifications(clients, serverInfo);
16
17
  // Register request handlers based on capabilities
17
- registerRequestHandlers(clients, server, capabilities);
18
+ registerRequestHandlers(clients, serverInfo, capabilities);
18
19
  return capabilities;
19
20
  }
20
21
  /**
@@ -24,9 +25,9 @@ export async function setupCapabilities(clients, server) {
24
25
  */
25
26
  function collectCapabilities(clients) {
26
27
  let capabilities = {};
27
- for (const [name, client] of Object.entries(clients)) {
28
+ for (const [name, clientInfo] of Object.entries(clients)) {
28
29
  try {
29
- const clientCapabilities = client.getServerCapabilities() || {};
30
+ const clientCapabilities = clientInfo.client.getServerCapabilities() || {};
30
31
  logger.debug(`Capabilities from ${name}: ${JSON.stringify(clientCapabilities)}`);
31
32
  capabilities = { ...capabilities, ...clientCapabilities };
32
33
  }
@@ -1,7 +1,9 @@
1
1
  import createClient from '../client.js';
2
2
  import logger from '../logger/logger.js';
3
3
  import { CONNECTION_RETRY, MCP_SERVER_NAME } from '../constants.js';
4
- import { ClientConnectionError, ClientNotFoundError, withErrorHandling } from '../utils/errorHandling.js';
4
+ import { withErrorHandling } from '../utils/errorHandling.js';
5
+ import { ClientConnectionError, ClientNotFoundError } from '../utils/errorTypes.js';
6
+ import { ClientStatus } from '../types.js';
5
7
  /**
6
8
  * Creates client instances for all transports with retry logic
7
9
  * @param transports Record of transport instances
@@ -15,15 +17,27 @@ export async function createClients(transports) {
15
17
  const client = await createClient();
16
18
  // Connect with retry logic
17
19
  await connectWithRetry(client, transport, name);
18
- clients[name] = client;
20
+ clients[name] = {
21
+ name,
22
+ transport,
23
+ client,
24
+ status: ClientStatus.Connected,
25
+ lastConnected: new Date(),
26
+ };
19
27
  logger.info(`Client created for ${name}`);
20
28
  }
21
29
  catch (error) {
22
30
  logger.error(`Failed to create client for ${name}: ${error}`);
23
- // We continue with other clients even if one fails
31
+ clients[name] = {
32
+ name,
33
+ transport,
34
+ client: await createClient(),
35
+ status: ClientStatus.Error,
36
+ lastError: error instanceof Error ? error : new Error(String(error)),
37
+ };
24
38
  }
25
39
  }
26
- return clients;
40
+ return Object.freeze(clients);
27
41
  }
28
42
  /**
29
43
  * Connects a client to its transport with retry logic
@@ -71,13 +85,30 @@ export function getClient(clients, clientName) {
71
85
  return client;
72
86
  }
73
87
  /**
74
- * Executes a client operation with error handling
88
+ * Executes a client operation with error handling and timeout
75
89
  * @param clients Record of client instances
76
90
  * @param clientName The name of the client to use
77
91
  * @param operation The operation to execute
92
+ * @param options Operation options including timeout and retry settings
78
93
  * @returns The result of the operation
79
94
  */
80
- export async function executeClientOperation(clients, clientName, operation) {
81
- const client = getClient(clients, clientName);
82
- return withErrorHandling(async () => operation(client), `Error executing operation on client ${clientName}`)();
95
+ export async function executeClientOperation(clients, clientName, operation, options = {}) {
96
+ const clientInfo = getClient(clients, clientName);
97
+ const { retryCount = 0, retryDelay = 1000 } = options;
98
+ return withErrorHandling(async () => {
99
+ let lastError;
100
+ for (let i = 0; i <= retryCount; i++) {
101
+ try {
102
+ return await operation(clientInfo);
103
+ }
104
+ catch (error) {
105
+ lastError = error instanceof Error ? error : new Error(String(error));
106
+ if (i < retryCount) {
107
+ logger.info(`Retrying operation ${operation.name} on client ${clientName} after ${retryDelay}ms`);
108
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
109
+ }
110
+ }
111
+ }
112
+ throw lastError;
113
+ }, `Error executing operation on client ${clientName}`)();
83
114
  }
@@ -127,4 +127,4 @@ export class ConfigManager extends EventEmitter {
127
127
  return { ...this.transportConfig };
128
128
  }
129
129
  }
130
- export default ConfigManager.getInstance();
130
+ export default ConfigManager;
@@ -0,0 +1,106 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ // Mock modules before importing ConfigManager
4
+ jest.mock('fs', () => ({
5
+ existsSync: jest.fn(),
6
+ mkdirSync: jest.fn(),
7
+ writeFileSync: jest.fn(),
8
+ readFileSync: jest.fn(),
9
+ watch: jest.fn(() => ({ close: jest.fn() })),
10
+ }));
11
+ const mockLogger = {
12
+ error: jest.fn(),
13
+ warn: jest.fn(),
14
+ info: jest.fn(),
15
+ debug: jest.fn(),
16
+ level: 'info',
17
+ transports: [],
18
+ };
19
+ jest.doMock('../logger/logger.js', () => ({
20
+ __esModule: true,
21
+ default: mockLogger,
22
+ }));
23
+ // Mock constants
24
+ jest.doMock('../constants.js', () => ({
25
+ __esModule: true,
26
+ DEFAULT_CONFIG: { mcpServers: {} },
27
+ getGlobalConfigPath: jest.fn(),
28
+ getGlobalConfigDir: jest.fn().mockReturnValue('/test'),
29
+ }));
30
+ // Import after mocks are set up
31
+ import { ConfigManager, ConfigChangeEvent } from './configManager.js';
32
+ import { DEFAULT_CONFIG } from '../constants.js';
33
+ describe('ConfigManager', () => {
34
+ const testConfigPath = '/test/config.json';
35
+ beforeEach(() => {
36
+ // Reset all mocks
37
+ jest.clearAllMocks();
38
+ // Reset singleton instance
39
+ ConfigManager.instance = undefined;
40
+ // Default mock implementations
41
+ fs.existsSync.mockReturnValue(true);
42
+ fs.readFileSync.mockReturnValue(JSON.stringify({ mcpServers: {} }));
43
+ });
44
+ describe('getInstance', () => {
45
+ it('should create singleton instance', () => {
46
+ const instance1 = ConfigManager.getInstance(testConfigPath);
47
+ const instance2 = ConfigManager.getInstance(testConfigPath);
48
+ expect(instance1).toBe(instance2);
49
+ });
50
+ it('should use provided config path', () => {
51
+ const instance = ConfigManager.getInstance(testConfigPath);
52
+ expect(instance.configFilePath).toBe(testConfigPath);
53
+ });
54
+ });
55
+ describe('ensureConfigExists', () => {
56
+ it('should create config directory and file if they do not exist', () => {
57
+ fs.existsSync
58
+ .mockReturnValueOnce(false) // Directory doesn't exist
59
+ .mockReturnValueOnce(false); // File doesn't exist
60
+ ConfigManager.getInstance(testConfigPath);
61
+ expect(fs.mkdirSync).toHaveBeenCalledWith(path.dirname(testConfigPath), { recursive: true });
62
+ expect(fs.writeFileSync).toHaveBeenCalledWith(testConfigPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
63
+ });
64
+ });
65
+ describe('config watching', () => {
66
+ it('should start and stop watching config file', () => {
67
+ const instance = ConfigManager.getInstance(testConfigPath);
68
+ instance.startWatching();
69
+ expect(fs.watch).toHaveBeenCalledWith(testConfigPath, expect.any(Function));
70
+ instance.stopWatching();
71
+ expect(instance['configWatcher']).toBeNull();
72
+ });
73
+ it('should reload config on file change', () => {
74
+ const instance = ConfigManager.getInstance(testConfigPath);
75
+ const mockWatcher = { close: jest.fn() };
76
+ let watchCallback = () => { };
77
+ fs.watch.mockImplementation((path, callback) => {
78
+ watchCallback = callback;
79
+ return mockWatcher;
80
+ });
81
+ instance.startWatching();
82
+ // Setup spy for event emission
83
+ const emitSpy = jest.spyOn(instance, 'emit');
84
+ // Mock new config data
85
+ const newConfig = { mcpServers: { test: { url: 'test-url' } } };
86
+ fs.readFileSync.mockReturnValueOnce(JSON.stringify(newConfig));
87
+ // Simulate file change
88
+ watchCallback('change', path.basename(testConfigPath));
89
+ expect(emitSpy).toHaveBeenCalledWith(ConfigChangeEvent.TRANSPORT_CONFIG_CHANGED, newConfig.mcpServers);
90
+ });
91
+ });
92
+ describe('getTransportConfig', () => {
93
+ it('should return copy of transport config', () => {
94
+ const testConfig = {
95
+ mcpServers: {
96
+ test: { url: 'test-url' },
97
+ },
98
+ };
99
+ fs.readFileSync.mockReturnValueOnce(JSON.stringify(testConfig));
100
+ const instance = ConfigManager.getInstance(testConfigPath);
101
+ const config = instance.getTransportConfig();
102
+ expect(config).toEqual(testConfig.mcpServers);
103
+ expect(config).not.toBe(instance['transportConfig']); // Should be a copy
104
+ });
105
+ });
106
+ });
@@ -1,74 +1,53 @@
1
1
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
2
2
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
3
- import { z, ZodError } from 'zod';
3
+ import { ZodError } from 'zod';
4
4
  import logger from '../logger/logger.js';
5
- /**
6
- * Zod schema for transport configuration
7
- */
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
- });
5
+ import { transportConfigSchema } from '../types.js';
21
6
  /**
22
7
  * Creates transport instances from configuration
23
8
  * @returns Record of transport instances
24
9
  */
25
- export function createTransports(mcpConfig) {
10
+ export function createTransports(config) {
26
11
  const transports = {};
27
- for (const [name, transport] of Object.entries(mcpConfig)) {
28
- if (transport.disabled) {
12
+ for (const [name, params] of Object.entries(config)) {
13
+ if (params.disabled) {
29
14
  logger.debug(`Skipping disabled transport: ${name}`);
30
15
  continue;
31
16
  }
32
17
  try {
33
- // Validate transport configuration
34
- const validatedTransport = transportConfigSchema.parse(transport);
35
- // Merge environment variables
36
- validatedTransport.env = {
37
- ...Object.fromEntries(Object.entries(process.env)
38
- .filter(([_, v]) => v !== undefined)
39
- .map(([k, v]) => [k, String(v)])),
40
- ...validatedTransport.env,
41
- };
42
- // Create transport based on type
18
+ const validatedTransport = transportConfigSchema.parse(params);
43
19
  if (validatedTransport.type === 'sse' || validatedTransport.type === 'http') {
44
20
  if (!validatedTransport.url) {
45
21
  throw new Error(`URL is required for SSE transport: ${name}`);
46
22
  }
47
- transports[name] = new SSEClientTransport(new URL(validatedTransport.url), {
23
+ const transport = new SSEClientTransport(new URL(validatedTransport.url), {
48
24
  requestInit: {
49
25
  headers: validatedTransport.headers,
50
26
  },
51
27
  });
28
+ transport.timeout = validatedTransport.timeout;
29
+ transport.tags = validatedTransport.tags;
30
+ transports[name] = transport;
52
31
  }
53
32
  else {
54
- // For stdio transport, ensure required fields are present
55
33
  if (!validatedTransport.command) {
56
34
  throw new Error(`Command is required for stdio transport: ${name}`);
57
35
  }
58
- transports[name] = new StdioClientTransport(validatedTransport);
36
+ const transport = new StdioClientTransport(validatedTransport);
37
+ transport.timeout = validatedTransport.timeout;
38
+ transport.tags = validatedTransport.tags;
39
+ transports[name] = transport;
59
40
  }
60
- logger.debug(`Created ${validatedTransport.type} transport for ${name}`);
41
+ logger.debug(`Created transport: ${name}`);
61
42
  }
62
43
  catch (error) {
63
44
  if (error instanceof ZodError) {
64
45
  logger.error(`Invalid transport configuration for ${name}:`, error.errors);
65
46
  }
66
- else if (error instanceof Error) {
67
- logger.error(`Failed to create transport for ${name}: ${error.message}`);
68
- }
69
47
  else {
70
- logger.error(`Failed to create transport for ${name}: ${String(error)}`);
48
+ logger.error(`Error creating transport ${name}:`, error);
71
49
  }
50
+ throw error;
72
51
  }
73
52
  }
74
53
  return transports;
@@ -3,6 +3,7 @@
3
3
  */
4
4
  // Server configuration
5
5
  export const PORT = 3050;
6
+ export const HOST = '127.0.0.1';
6
7
  // API endpoints
7
8
  export const SSE_ENDPOINT = '/sse';
8
9
  export const MESSAGES_ENDPOINT = '/messages';
@@ -22,7 +23,7 @@ export const ERROR_CODES = {
22
23
  // MCP constants
23
24
  export const MCP_CONFIG_FILE = 'mcp.json';
24
25
  export const MCP_SERVER_NAME = '1mcp';
25
- export const MCP_SERVER_VERSION = '0.2.0';
26
+ export const MCP_SERVER_VERSION = '0.4.0';
26
27
  export const MCP_URI_SEPARATOR = '_1mcp_';
27
28
  // Global config paths
28
29
  export const CONFIG_DIR_NAME = '1mcp';
@@ -4,9 +4,9 @@ import { withErrorHandling } from '../utils/errorHandling.js';
4
4
  /**
5
5
  * Sets up client-to-server notification handlers
6
6
  * @param clients Record of client instances
7
- * @param server The MCP server instance
7
+ * @param serverInfo The MCP server instance
8
8
  */
9
- export function setupClientToServerNotifications(clients, server) {
9
+ export function setupClientToServerNotifications(clients, serverInfo) {
10
10
  const clientNotificationSchemas = [
11
11
  CancelledNotificationSchema,
12
12
  ProgressNotificationSchema,
@@ -16,11 +16,11 @@ export function setupClientToServerNotifications(clients, server) {
16
16
  ToolListChangedNotificationSchema,
17
17
  PromptListChangedNotificationSchema,
18
18
  ];
19
- for (const [name, client] of Object.entries(clients)) {
19
+ for (const [name, clientInfo] of Object.entries(clients)) {
20
20
  clientNotificationSchemas.forEach((schema) => {
21
- client.setNotificationHandler(schema, withErrorHandling(async (notification) => {
21
+ clientInfo.client.setNotificationHandler(schema, withErrorHandling(async (notification) => {
22
22
  logger.info(`Received notification in client: ${name} ${JSON.stringify(notification)}`);
23
- server.notification({
23
+ serverInfo.server.notification({
24
24
  ...notification,
25
25
  params: {
26
26
  ...notification.params,
@@ -34,20 +34,20 @@ export function setupClientToServerNotifications(clients, server) {
34
34
  /**
35
35
  * Sets up server-to-client notification handlers
36
36
  * @param clients Record of client instances
37
- * @param server The MCP server instance
37
+ * @param serverInfo The MCP server instance
38
38
  */
39
- export function setupServerToClientNotifications(clients, server) {
39
+ export function setupServerToClientNotifications(clients, serverInfo) {
40
40
  const serverNotificationSchemas = [
41
41
  CancelledNotificationSchema,
42
42
  ProgressNotificationSchema,
43
43
  InitializedNotificationSchema,
44
44
  RootsListChangedNotificationSchema,
45
45
  ];
46
- for (const [name, client] of Object.entries(clients)) {
46
+ for (const [name, clientInfo] of Object.entries(clients)) {
47
47
  serverNotificationSchemas.forEach((schema) => {
48
- server.setNotificationHandler(schema, withErrorHandling(async (notification) => {
48
+ serverInfo.server.setNotificationHandler(schema, withErrorHandling(async (notification) => {
49
49
  logger.info(`Received notification in server: ${name} ${JSON.stringify(notification)}`);
50
- client.notification({
50
+ clientInfo.client.notification({
51
51
  ...notification,
52
52
  params: {
53
53
  ...notification.params,