Skip to content

Commit d71d383

Browse files
committed
feat: add workspace management and multi-config support
- Support multiple --config files with intelligent merging - Add global workspace cache to track active hub instances - New /api/workspaces endpoint and workspaces_updated SSE event - Enhanced /api/health with workspace information - Foundation for project-specific hub instances
1 parent 2cab9d2 commit d71d383

File tree

9 files changed

+578
-62
lines changed

9 files changed

+578
-62
lines changed

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88

9+
## [4.1.0] - 2025-07-15
10+
11+
### Added
12+
13+
- **Workspace Management**: Global workspace cache to track active hub instances across different working directories
14+
- New `/api/workspaces` endpoint to list all active workspace instances
15+
- Real-time workspace updates via new `workspaces_updated` SSE subscription event
16+
- XDG-compliant workspace cache storage (`~/.local/state/mcp-hub/workspaces.json`)
17+
- Automatic cleanup of stale workspace entries
18+
19+
- **Multi-Configuration File Support**: Enhanced configuration system with intelligent merging
20+
- CLI now accepts multiple `--config` arguments: `--config global.json --config project.json`
21+
- Later configuration files override earlier ones with smart merging rules
22+
- Missing configuration files are gracefully skipped
23+
- Enhanced file watching for all specified configuration files
24+
25+
- **Global Environment Variable Injection**: `MCP_HUB_ENV` environment variable support
26+
- Parse JSON string from `MCP_HUB_ENV` and inject key-value pairs into all MCP server environments
27+
- Useful for passing shared secrets, tokens, or configuration to all servers
28+
- Server-specific `env` fields always override global values
29+
30+
### Enhanced
31+
32+
- **Health Endpoint**: Now includes comprehensive workspace information showing current workspace and all active instances
33+
- **Configuration Management**: Enhanced ConfigManager with robust array-based config path handling
34+
- **File Watching**: Improved configuration file monitoring across multiple files with better change detection
35+
- **Error Handling**: Enhanced error handling and logging throughout the workspace management system
36+
- Updated all dependencies to latest versions for improved security and performance
37+
938
## [4.0.0] - 2025-07-09
1039

1140
### Breaking Changes

README.md

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ The Hub automatically:
105105
- Proper cleanup of server connections
106106
- Error recovery and reconnection
107107

108+
- **Workspace Management**:
109+
- Track active MCP Hub instances across different working directories
110+
- Global workspace cache in XDG-compliant state directory
111+
- Real-time workspace updates via SSE events
112+
- API endpoints to list and monitor active workspaces
113+
108114
### Components
109115

110116
#### Hub Server
@@ -137,13 +143,16 @@ Start the hub server:
137143

138144
```bash
139145
mcp-hub --port 3000 --config path/to/config.json
146+
147+
# Or with multiple config files (merged in order)
148+
mcp-hub --port 3000 --config ~/.config/mcphub/global.json --config ./.mcphub/project.json
140149
```
141150

142151
### CLI Options
143152
```bash
144153
Options:
145154
--port Port to run the server on (required)
146-
--config Path to config file (required)
155+
--config Path to config file(s). Can be specified multiple times. Merged in order. (required)
147156
--watch Watch config file for changes, only updates affected servers (default: false)
148157
--auto-shutdown Whether to automatically shutdown when no clients are connected (default: false)
149158
--shutdown-delay Delay in milliseconds before shutting down when auto-shutdown is enabled (default: 0)
@@ -152,7 +161,27 @@ Options:
152161

153162
## Configuration
154163

155-
MCP Hub uses a JSON configuration file to define managed servers with **universal `${}` placeholder syntax** for environment variables and command execution.
164+
MCP Hub uses JSON configuration files to define managed servers with **universal `${}` placeholder syntax** for environment variables and command execution.
165+
166+
### Multiple Configuration Files
167+
168+
MCP Hub supports loading multiple configuration files that are merged in order. This enables flexible configuration management:
169+
170+
- **Global Configuration**: System-wide settings (e.g., `~/.config/mcphub/global.json`)
171+
- **Project Configuration**: Project-specific settings (e.g., `./.mcphub/project.json`)
172+
- **Environment Configuration**: Environment-specific overrides
173+
174+
When multiple config files are specified, they are merged with later files overriding earlier ones:
175+
176+
```bash
177+
# Global config is loaded first, then project config overrides
178+
mcp-hub --port 3000 --config ~/.config/mcphub/global.json --config ./.mcphub/project.json
179+
```
180+
181+
**Merge Behavior:**
182+
- `mcpServers` sections are merged (server definitions from later files override earlier ones)
183+
- Other top-level properties are completely replaced by later files
184+
- Missing config files are silently skipped
156185

157186
### Universal Placeholder Syntax
158187

@@ -353,6 +382,16 @@ Response:
353382
"lastEventAt": "2024-02-20T05:55:00.000Z"
354383
}
355384
]
385+
},
386+
"workspace": {
387+
"current": "/path/to/current/project",
388+
"allActive": {
389+
"/path/to/project-a": {
390+
"pid": 12345,
391+
"port": 40123,
392+
"startTime": "2025-01-01T12:00:00.000Z"
393+
}
394+
}
356395
}
357396
}
358397
```
@@ -479,6 +518,34 @@ Response:
479518
}
480519
```
481520

521+
### Workspace Management
522+
523+
#### List Active Workspaces
524+
525+
```bash
526+
GET /api/workspaces
527+
```
528+
529+
Response:
530+
531+
```json
532+
{
533+
"workspaces": {
534+
"/path/to/project-a": {
535+
"pid": 12345,
536+
"port": 40123,
537+
"startTime": "2025-01-01T12:00:00.000Z"
538+
},
539+
"/path/to/project-b": {
540+
"pid": 54321,
541+
"port": 40567,
542+
"startTime": "2025-01-01T12:05:00.000Z"
543+
}
544+
},
545+
"timestamp": "2024-02-20T05:55:00.000Z"
546+
}
547+
```
548+
482549
### Marketplace Integration
483550

484551
#### List Available Servers
@@ -766,6 +833,22 @@ MCP Hub emits several types of events:
766833
"timestamp": "2024-02-20T05:55:00.000Z"
767834
}
768835
```
836+
837+
7. **workspaces_updated** - Active workspaces changed
838+
```json
839+
{
840+
"type": "workspaces_updated",
841+
"workspaces": {
842+
"/path/to/project-a": {
843+
"pid": 12345,
844+
"port": 40123,
845+
"startTime": "2025-01-01T12:00:00.000Z"
846+
}
847+
},
848+
"timestamp": "2024-02-20T05:55:00.000Z"
849+
}
850+
```
851+
769852
### Connection Management
770853

771854
- Each SSE connection is assigned a unique ID
@@ -780,6 +863,15 @@ MCP Hub uses structured JSON logging for all events. Logs are written to both co
780863
- **XDG compliant**: `$XDG_STATE_HOME/mcp-hub/logs/mcp-hub.log` (typically `~/.local/state/mcp-hub/logs/mcp-hub.log`)
781864
- **Legacy fallback**: `~/.mcp-hub/logs/mcp-hub.log` (for backward compatibility)
782865

866+
## Workspace Cache
867+
868+
MCP Hub maintains a global workspace cache to track active instances across different working directories:
869+
870+
- **Cache Location**: `$XDG_STATE_HOME/mcp-hub/workspaces.json` (typically `~/.local/state/mcp-hub/workspaces.json`)
871+
- **Purpose**: Prevents port conflicts and enables workspace discovery
872+
- **Content**: Maps workspace paths to their corresponding hub process information (PID, port, start time)
873+
- **Cleanup**: Automatically removes stale entries when processes are no longer running
874+
783875
```json
784876
{
785877
"type": "error",

src/MCPConnection.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,7 @@ export class MCPConnection extends EventEmitter {
656656
lastStarted: this.lastStarted,
657657
authorizationUrl: this.authorizationUrl,
658658
serverInfo: this.serverInfo, // Include server's reported name/version
659+
config_source: this.config.config_source, // Include which config file this server came from
659660
};
660661
}
661662

src/MCPHub.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class MCPHub extends EventEmitter {
1616
this.port = port;
1717
this.hubServerUrl = `http://localhost:${port}`;
1818
this.configManager = new ConfigManager(configPathOrObject);
19-
this.shouldWatchConfig = watch && typeof configPathOrObject === "string";
19+
this.shouldWatchConfig = watch && (typeof configPathOrObject === "string" || Array.isArray(configPathOrObject));
2020
this.marketplace = marketplace;
2121
}
2222
async initialize(isRestarting) {
@@ -207,6 +207,7 @@ export class MCPHub extends EventEmitter {
207207
...removePromises,
208208
...modifiedPromises,
209209
])
210+
this.emit("importantConfigChangeHandled", changes);
210211
} catch (error) {
211212
logger.error(
212213
error.code || "CONFIG_UPDATE_ERROR",
@@ -217,7 +218,6 @@ export class MCPHub extends EventEmitter {
217218
},
218219
false
219220
)
220-
} finally {
221221
this.emit("importantConfigChangeHandled", changes);
222222
}
223223
}

src/server.js

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "./utils/errors.js";
1616
import { getMarketplace } from "./marketplace.js";
1717
import { MCPServerEndpoint } from "./mcp/server.js";
18+
import { WorkspaceCacheManager } from "./utils/workspace-cache.js";
1819

1920
const SERVER_ID = "mcp-hub";
2021

@@ -48,6 +49,7 @@ class ServiceManager {
4849
this.mcpHub = null;
4950
this.server = null;
5051
this.sseManager = new SSEManager(options);
52+
this.workspaceCache = new WorkspaceCacheManager();
5153
this.state = 'starting';
5254
// Connect logger to SSE manager
5355
logger.setSseManager(this.sseManager);
@@ -93,7 +95,18 @@ class ServiceManager {
9395
}
9496

9597
async initializeMCPHub() {
96-
// Initialize marketplace first
98+
// Initialize workspace cache first
99+
logger.info("Initializing workspace cache");
100+
await this.workspaceCache.initialize();
101+
await this.workspaceCache.register(this.port);
102+
await this.workspaceCache.startWatching();
103+
104+
// Setup workspace cache event handlers
105+
this.workspaceCache.on('workspacesUpdated', (workspaces) => {
106+
this.broadcastSubscriptionEvent(SubscriptionTypes.WORKSPACES_UPDATED, { workspaces });
107+
});
108+
109+
// Initialize marketplace second
97110
logger.info("Initializing marketplace catalog");
98111
marketplace = getMarketplace();
99112
await marketplace.initialize();
@@ -287,7 +300,17 @@ class ServiceManager {
287300
}
288301
}
289302

290-
await Promise.allSettled([this.stopMCPHub(), this.sseManager.shutdown(), this.stopServer()]);
303+
//INFO:Sometimes this might take some time, keeping the process alive, this might cause issue when restarting
304+
//INFO: MUST catch the error here to avoid unhandled rejection, which will again call shutdown() leading to infinite loop
305+
this.stopServer().catch((error) => {
306+
// Mostly happens when the server is already stopped (race condition)
307+
logger.debug(`Error stopping HTTP server: ${error.message}`);
308+
})
309+
await Promise.allSettled([
310+
this.stopMCPHub(),
311+
this.sseManager.shutdown(),
312+
this.workspaceCache.shutdown()
313+
]);
291314
this.setState(HubState.STOPPED)
292315
}
293316
}
@@ -301,7 +324,7 @@ registerRoute("GET", "/events", "Subscribe to server events", (req, res) => {
301324
// Add client connection
302325
const connection = serviceManager.sseManager.addConnection(req, res);
303326
// Send initial state
304-
serviceManager.broadcastHubState();
327+
connection.send(EventTypes.HUB_STATE, serviceManager.getState());
305328
} catch (error) {
306329
logger.error('SSE_SETUP_ERROR', 'Failed to setup SSE connection', {
307330
error: error.message,
@@ -399,6 +422,26 @@ registerRoute(
399422
}
400423
);
401424

425+
// Register workspaces endpoint
426+
registerRoute(
427+
"GET",
428+
"/workspaces",
429+
"Get all active workspaces",
430+
async (req, res) => {
431+
try {
432+
const workspaces = await serviceManager.workspaceCache.getActiveWorkspaces();
433+
res.json({
434+
workspaces,
435+
timestamp: new Date().toISOString(),
436+
});
437+
} catch (error) {
438+
throw wrapError(error, "WORKSPACE_ERROR", {
439+
operation: "list_workspaces",
440+
});
441+
}
442+
}
443+
);
444+
402445
// Register server start endpoint
403446
registerRoute(
404447
"POST",
@@ -462,7 +505,7 @@ registerRoute(
462505
);
463506

464507
// Register health check endpoint
465-
registerRoute("GET", "/health", "Check server health", (req, res) => {
508+
registerRoute("GET", "/health", "Check server health", async (req, res) => {
466509
const healthData = {
467510
status: "ok",
468511
state: serviceManager?.state || HubState.STARTING,
@@ -478,6 +521,18 @@ registerRoute("GET", "/health", "Check server health", (req, res) => {
478521
healthData.mcpEndpoint = mcpServerEndpoint.getStats();
479522
}
480523

524+
// Add workspace information if available
525+
if (serviceManager?.workspaceCache) {
526+
try {
527+
healthData.workspace = {
528+
current: serviceManager.workspaceCache.getWorkspaceKey(),
529+
allActive: await serviceManager.workspaceCache.getActiveWorkspaces()
530+
};
531+
} catch (error) {
532+
logger.debug(`Failed to get workspace info for health check: ${error.message}`);
533+
}
534+
}
535+
481536
res.json(healthData);
482537
});
483538

src/utils/cli.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ async function run() {
5858
},
5959
config: {
6060
alias: "c",
61-
describe: "Path to config file",
62-
type: "string",
61+
describe: "Path to config file(s). Can be specified multiple times. Merged in order.",
62+
type: "array",
6363
demandOption: true,
6464
},
6565
watch: {
@@ -80,15 +80,15 @@ async function run() {
8080
default: 0,
8181
},
8282
})
83-
.example("mcp-hub --port 3000 --config ./mcp-servers.json")
83+
.example("mcp-hub --port 3000 --config ./global.json --config ./project.json")
8484
.help("h")
8585
.alias("h", "help")
8686
.fail(handleParseError).argv;
8787

8888
try {
8989
await startServer({
9090
port: argv.port,
91-
config: argv.config,
91+
config: argv.config, // This will now be an array of paths
9292
watch: argv.watch,
9393
autoShutdown: argv["auto-shutdown"],
9494
shutdownDelay: argv["shutdown-delay"],

0 commit comments

Comments
 (0)