imcp 0.0.9 → 0.0.11

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
@@ -1,6 +1,6 @@
1
1
  # IMCP
2
2
 
3
- IMCP is a single-line pull and push experience that makes onboarding MCP servers easy.
3
+ IMCP is a Node.js SDK for MCP servers, offering a streamlined, powerful experience of managing MCP servers to your agents (e.g. code agents such as Cline/Github Copilot/Roo), through a unified interface – currently we support coding, browser and Bing tools but can expand more. Let’s use it to weapon your agents
4
4
 
5
5
  ## Overview
6
6
 
@@ -10,7 +10,7 @@ IMCP allows you to:
10
10
  - Run a local UI interface with simple click experience to manage MCP servers
11
11
  - (in progress) Distribute your own MCP servers to others
12
12
 
13
- ## Installation
13
+ ## Get started
14
14
  - Quick usage with latest version
15
15
  ```
16
16
  npx -y imcp@latest serve
@@ -20,6 +20,9 @@ npx -y imcp@latest serve
20
20
  ```bash
21
21
  npm install -g imcp
22
22
  ```
23
+ ```
24
+ imcp serve
25
+ ```
23
26
 
24
27
  ## Commands
25
28
 
@@ -37,105 +40,17 @@ imcp serve [options]
37
40
 
38
41
  Options:
39
42
  - `-p, --port <port>`: Port to run the server on (default: 3000)
43
+ - `-f, --feed-file <filepath>`: Path to a custom feed configuration file
40
44
 
41
45
  Example:
42
46
  ```bash
43
47
  # Start the web interface on port 8080
44
48
  imcp serve --port 8080
45
- ```
46
-
47
- ### list
48
-
49
- Lists all available MCP servers from configured feeds.
50
-
51
- ```bash
52
- imcp list [options]
53
- ```
54
-
55
- Options:
56
- - `--pull`: Sync with remote feeds before listing
57
49
 
58
- Example:
59
- ```bash
60
- # List servers after syncing with remote feeds
61
- imcp list --pull
50
+ # Start with a custom feed configuration file
51
+ imcp serve --feed-file ./custom-feed.json
62
52
  ```
63
53
 
64
- ### install
65
-
66
- Installs specific MCP servers.
67
-
68
- ```bash
69
- imcp install [options]
70
- ```
71
-
72
- Options:
73
- - `--category <category>`: Server category (required)
74
- - `--name <name>`: Server name to install (required)
75
- - `--force`: Force installation even if server already exists
76
- - `--clients <clients>`: Target clients (semicolon separated). Supported values: Cline, MSRooCode, GithubCopilot
77
- - `--envs <envs>`: Environment variables (semicolon separated key=value pairs)
78
-
79
- Examples:
80
- ```bash
81
- # Install a server
82
- imcp install --category ai-coder-tools --name git-tools
83
-
84
- # Install with specific client targets
85
- imcp install --category ai-coder-tools --name git-tools --clients "MSRooCode;GithubCopilot"
86
-
87
- # Install with environment variables
88
- imcp install --category ai-coder-tools --name git-tools --envs "GITHUB_TOKEN=abc123;API_KEY=xyz789"
89
- ```
90
-
91
- ### pull
92
-
93
- Pulls MCP server configurations from remote feed sources to update local feeds.
94
-
95
- ```bash
96
- imcp pull
97
- ```
98
-
99
- Example:
100
- ```bash
101
- # Update local feeds with the latest server configurations
102
- imcp pull
103
- ```
104
-
105
- ## Development
106
-
107
- ### Prerequisites
108
-
109
- - Node.js
110
- - npm
111
-
112
- ### Setup
113
-
114
- ```bash
115
- # Clone the repository
116
- git clone <repository-url>
117
- cd imcp
118
-
119
- # Install dependencies
120
- npm install
121
-
122
- # Build the project
123
- npm run build
124
-
125
- # Run in development mode
126
- npm run dev
127
- ```
128
-
129
- ### Scripts
130
-
131
- - `npm run build`: Builds the project and copies necessary files
132
- - `npm run dev`: Runs TypeScript in watch mode
133
- - `npm run start`: Starts the web server
134
- - `npm run dev:server`: Starts the development server with hot reloading
135
- - `npm run test`: Runs tests
136
- - `npm run lint`: Lints the source code
137
- - `npm run format`: Formats the source code
138
-
139
54
  ## License
140
55
 
141
56
  MIT
@@ -5,12 +5,13 @@ export function createServeCommand() {
5
5
  return new Command('serve')
6
6
  .description('Serve local web interface')
7
7
  .option('-p, --port <port>', 'Port to run the server on', '3000')
8
+ .option('-f, --feed-file <filepath>', 'Path to a custom feed configuration file')
8
9
  .action(async (options) => {
9
10
  try {
10
11
  // Sync feeds before start the local UI
11
12
  await mcpManager.syncFeeds();
12
13
  // Ensure MCP manager is initialized before starting the web server
13
- await mcpManager.initialize();
14
+ await mcpManager.initialize(options.feedFile);
14
15
  const port = parseInt(options.port, 10);
15
16
  if (isNaN(port) || port < 1 || port > 65535) {
16
17
  throw new Error('Invalid port number');
package/dist/cli/index.js CHANGED
@@ -1,9 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { createServeCommand } from './commands/serve.js';
4
- import { createListCommand } from './commands/list.js';
5
- import { createInstallCommand } from './commands/install.js';
6
- import { createPullCommand } from './commands/pull.js';
7
4
  import { Logger } from '../utils/logger.js';
8
5
  import axios from 'axios';
9
6
  import path from 'path';
@@ -30,10 +27,10 @@ async function main() {
30
27
  Logger.setVerbose(!!opts.verbose);
31
28
  // Add all commands
32
29
  program.addCommand(createServeCommand());
33
- program.addCommand(createListCommand());
34
- program.addCommand(createInstallCommand());
30
+ // program.addCommand(createListCommand());
31
+ // program.addCommand(createInstallCommand());
35
32
  // program.addCommand(createUninstallCommand());
36
- program.addCommand(createPullCommand());
33
+ // program.addCommand(createPullCommand());
37
34
  // Error handling for the entire CLI
38
35
  program.exitOverride();
39
36
  // Check for updates
@@ -24,7 +24,7 @@ export declare class ConfigurationLoader {
24
24
  /**
25
25
  * Loads feed configurations into the MCP configuration
26
26
  */
27
- static loadFeedsIntoConfiguration(configuration: MCPConfiguration): Promise<MCPConfiguration>;
27
+ static loadFeedsIntoConfiguration(configuration: MCPConfiguration, feedFile?: string): Promise<MCPConfiguration>;
28
28
  /**
29
29
  * Loads MCP client settings into the configuration
30
30
  */
@@ -140,23 +140,44 @@ export class ConfigurationLoader {
140
140
  /**
141
141
  * Loads feed configurations into the MCP configuration
142
142
  */
143
- static async loadFeedsIntoConfiguration(configuration) {
143
+ static async loadFeedsIntoConfiguration(configuration, feedFile) {
144
144
  try {
145
145
  await fs.mkdir(LOCAL_FEEDS_DIR, { recursive: true });
146
+ const feeds = {};
147
+ // Load provided feed file if specified
148
+ if (feedFile) {
149
+ try {
150
+ const content = await fs.readFile(feedFile, 'utf8');
151
+ const config = JSON.parse(content);
152
+ if (config && config.name) {
153
+ feeds[config.name] = config;
154
+ console.log(`Loaded feed configuration from provided file: ${feedFile}`);
155
+ }
156
+ }
157
+ catch (error) {
158
+ console.log(`Error loading feed configuration from provided file ${feedFile}:`, error);
159
+ }
160
+ }
161
+ // Load feeds from LOCAL_FEEDS_DIR
146
162
  const files = await fs.readdir(LOCAL_FEEDS_DIR);
147
163
  const jsonFiles = files.filter(file => file.endsWith('.json'));
148
- if (jsonFiles.length === 0) {
164
+ if (jsonFiles.length === 0 && !feedFile) {
149
165
  console.log(`No feed configuration files found in ${LOCAL_FEEDS_DIR}`);
150
166
  return configuration;
151
167
  }
152
- const feeds = {};
153
168
  for (const file of jsonFiles) {
154
169
  try {
155
170
  const filePath = path.join(LOCAL_FEEDS_DIR, file);
156
171
  const content = await fs.readFile(filePath, 'utf8');
157
172
  const config = JSON.parse(content);
158
173
  if (config && config.name) {
159
- feeds[config.name] = config;
174
+ // If feed exists from provided file, skip the local one
175
+ if (!feeds[config.name]) {
176
+ feeds[config.name] = config;
177
+ }
178
+ else {
179
+ console.log(`Skipping local feed ${config.name} as it was provided via --feed-file`);
180
+ }
160
181
  }
161
182
  }
162
183
  catch (error) {
@@ -8,7 +8,7 @@ export declare class ConfigurationProvider {
8
8
  private constructor();
9
9
  static getInstance(): ConfigurationProvider;
10
10
  private withLock;
11
- initialize(): Promise<void>;
11
+ initialize(feedFile?: string): Promise<void>;
12
12
  private saveConfiguration;
13
13
  getServerCategories(): Promise<MCPServerCategory[]>;
14
14
  getServerCategory(categoryName: string): Promise<MCPServerCategory | undefined>;
@@ -43,7 +43,7 @@ export class ConfigurationProvider {
43
43
  resolve();
44
44
  }
45
45
  }
46
- async initialize() {
46
+ async initialize(feedFile) {
47
47
  await this.withLock(async () => {
48
48
  const configDir = path.dirname(this.configPath);
49
49
  await fs.mkdir(configDir, { recursive: true });
@@ -60,7 +60,7 @@ export class ConfigurationProvider {
60
60
  await this.saveConfiguration();
61
61
  }
62
62
  // Always load feeds and client settings, whether file existed or not
63
- await this.loadFeedsIntoConfiguration();
63
+ await this.loadFeedsIntoConfiguration(feedFile);
64
64
  await this.loadClientMCPSettings();
65
65
  }
66
66
  catch (error) {
@@ -280,8 +280,8 @@ export class ConfigurationProvider {
280
280
  }
281
281
  });
282
282
  }
283
- async loadFeedsIntoConfiguration() {
284
- this.configuration = await ConfigurationLoader.loadFeedsIntoConfiguration(this.configuration);
283
+ async loadFeedsIntoConfiguration(feedFile) {
284
+ this.configuration = await ConfigurationLoader.loadFeedsIntoConfiguration(this.configuration, feedFile);
285
285
  await this.saveConfiguration();
286
286
  }
287
287
  async loadClientMCPSettings() {
@@ -5,7 +5,7 @@ export declare class MCPManager extends EventEmitter {
5
5
  private configProvider;
6
6
  constructor();
7
7
  syncFeeds(): Promise<void>;
8
- initialize(): Promise<void>;
8
+ initialize(feedFile?: string): Promise<void>;
9
9
  listServerCategories(options?: ServerCategoryListOptions): Promise<MCPServerCategory[]>;
10
10
  getFeedConfiguration(categoryName: string): Promise<import("./types.js").FeedConfiguration | undefined>;
11
11
  installServer(categoryName: string, serverName: string, requestOptions?: ServerInstallOptions): Promise<ServerOperationResult>;
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { ConfigurationProvider } from './ConfigurationProvider.js';
3
3
  import { InstallationService } from './InstallationService.js';
4
- import { MCPEvent } from './types.js';
4
+ import { MCPEvent, } from './types.js';
5
5
  export class MCPManager extends EventEmitter {
6
6
  installationService;
7
7
  configProvider;
@@ -13,9 +13,9 @@ export class MCPManager extends EventEmitter {
13
13
  async syncFeeds() {
14
14
  await this.configProvider.syncFeeds();
15
15
  }
16
- async initialize() {
16
+ async initialize(feedFile) {
17
17
  try {
18
- await this.configProvider.initialize();
18
+ await this.configProvider.initialize(feedFile);
19
19
  }
20
20
  catch (error) {
21
21
  console.error("Error during MCPManager initialization:", error);
@@ -0,0 +1,11 @@
1
+ import { ServerSchema } from './ServerSchemaProvider.js';
2
+ export declare class ServerSchemaLoader {
3
+ /**
4
+ * Load schema for a specific server in a category
5
+ */
6
+ static loadSchema(categoryName: string, serverName: string): Promise<ServerSchema | undefined>;
7
+ /**
8
+ * Validate schema content against expected format
9
+ */
10
+ static validateSchema(schema: any): boolean;
11
+ }
@@ -0,0 +1,43 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { LOCAL_FEEDS_DIR } from './constants.js';
4
+ import { Logger } from '../utils/logger.js';
5
+ export class ServerSchemaLoader {
6
+ /**
7
+ * Load schema for a specific server in a category
8
+ */
9
+ static async loadSchema(categoryName, serverName) {
10
+ try {
11
+ const schemaPath = path.join(LOCAL_FEEDS_DIR, categoryName, `${serverName}.json`);
12
+ const content = await fs.readFile(schemaPath, 'utf8');
13
+ const schema = JSON.parse(content);
14
+ // Validate schema structure
15
+ if (!schema.version || !schema.schema) {
16
+ Logger.debug(`Invalid schema format for server ${serverName} in category ${categoryName}`);
17
+ return undefined;
18
+ }
19
+ return {
20
+ schema: schema
21
+ };
22
+ }
23
+ catch (error) {
24
+ if (error.code === 'ENOENT') {
25
+ Logger.debug(`No schema file found for server ${serverName} in category ${categoryName}`);
26
+ return undefined;
27
+ }
28
+ Logger.error(`Error loading schema for server ${serverName} in category ${categoryName}:`, error);
29
+ throw error;
30
+ }
31
+ }
32
+ /**
33
+ * Validate schema content against expected format
34
+ */
35
+ static validateSchema(schema) {
36
+ return (typeof schema === 'object' &&
37
+ schema !== null &&
38
+ typeof schema.version === 'string' &&
39
+ typeof schema.schema === 'object' &&
40
+ schema.schema !== null);
41
+ }
42
+ }
43
+ //# sourceMappingURL=ServerSchemaLoader.js.map
@@ -0,0 +1,17 @@
1
+ export interface ServerSchema {
2
+ schema: Record<string, any>;
3
+ }
4
+ export declare class ServerSchemaProvider {
5
+ private static instance;
6
+ private schemaMap;
7
+ private schemaLock;
8
+ private constructor();
9
+ static getInstance(): Promise<ServerSchemaProvider>;
10
+ private withLock;
11
+ initialize(): Promise<void>;
12
+ private loadSchema;
13
+ private loadAllSchemas;
14
+ getSchema(categoryName: string, serverName: string): Promise<ServerSchema | undefined>;
15
+ reloadSchemas(): Promise<void>;
16
+ }
17
+ export declare function getServerSchemaProvider(): Promise<ServerSchemaProvider>;
@@ -0,0 +1,115 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { LOCAL_FEEDS_SCHEMA_DIR } from './constants.js';
4
+ import { Logger } from '../utils/logger.js';
5
+ export class ServerSchemaProvider {
6
+ static instance;
7
+ schemaMap;
8
+ schemaLock = Promise.resolve();
9
+ constructor() {
10
+ this.schemaMap = new Map();
11
+ }
12
+ static async getInstance() {
13
+ if (!ServerSchemaProvider.instance) {
14
+ ServerSchemaProvider.instance = new ServerSchemaProvider();
15
+ await ServerSchemaProvider.instance.initialize();
16
+ }
17
+ return ServerSchemaProvider.instance;
18
+ }
19
+ async withLock(operation) {
20
+ const current = this.schemaLock;
21
+ let resolve;
22
+ this.schemaLock = new Promise(r => resolve = r);
23
+ try {
24
+ await current;
25
+ return await operation();
26
+ }
27
+ finally {
28
+ resolve();
29
+ }
30
+ }
31
+ async initialize() {
32
+ await this.withLock(async () => {
33
+ try {
34
+ // Create feeds directory if it doesn't exist
35
+ await fs.mkdir(LOCAL_FEEDS_SCHEMA_DIR, { recursive: true });
36
+ // Load all schemas from the feeds directory
37
+ await this.loadAllSchemas();
38
+ }
39
+ catch (error) {
40
+ Logger.error('Error during schema initialization:', error);
41
+ throw error;
42
+ }
43
+ });
44
+ }
45
+ async loadSchema(categoryName, serverName) {
46
+ try {
47
+ const schemaPath = path.join(LOCAL_FEEDS_SCHEMA_DIR, categoryName, `${serverName}.json`);
48
+ const content = await fs.readFile(schemaPath, 'utf8');
49
+ const schema = JSON.parse(content);
50
+ return {
51
+ schema: schema
52
+ };
53
+ }
54
+ catch (error) {
55
+ if (error.code === 'ENOENT') {
56
+ Logger.debug(`No schema file found for server ${serverName} in category ${categoryName}`);
57
+ return undefined;
58
+ }
59
+ Logger.error(`Error loading schema for server ${serverName} in category ${categoryName}:`, error);
60
+ throw error;
61
+ }
62
+ }
63
+ async loadAllSchemas() {
64
+ this.schemaMap.clear();
65
+ // Read server category directories
66
+ const categoryDirs = await fs.readdir(LOCAL_FEEDS_SCHEMA_DIR, { withFileTypes: true });
67
+ for (const categoryDir of categoryDirs) {
68
+ if (categoryDir.isDirectory()) {
69
+ const categoryPath = path.join(LOCAL_FEEDS_SCHEMA_DIR, categoryDir.name);
70
+ const serverFiles = await fs.readdir(categoryPath);
71
+ const serverSchemas = new Map();
72
+ for (const file of serverFiles) {
73
+ if (file.endsWith('.json')) {
74
+ const serverName = path.basename(file, '.json');
75
+ try {
76
+ const schema = await this.loadSchema(categoryDir.name, serverName);
77
+ if (schema) {
78
+ serverSchemas.set(serverName, schema);
79
+ }
80
+ }
81
+ catch (error) {
82
+ Logger.error(`Error loading schema for server ${serverName} in category ${categoryDir.name}:`, error);
83
+ }
84
+ }
85
+ }
86
+ if (serverSchemas.size > 0) {
87
+ this.schemaMap.set(categoryDir.name, serverSchemas);
88
+ }
89
+ }
90
+ }
91
+ }
92
+ async getSchema(categoryName, serverName) {
93
+ return await this.withLock(async () => {
94
+ const categorySchemas = this.schemaMap.get(categoryName);
95
+ if (!categorySchemas) {
96
+ return undefined;
97
+ }
98
+ return categorySchemas.get(serverName);
99
+ });
100
+ }
101
+ async reloadSchemas() {
102
+ return await this.withLock(async () => {
103
+ await this.loadAllSchemas();
104
+ });
105
+ }
106
+ }
107
+ // Export a lazy initialized singleton instance getter
108
+ let initPromise = null;
109
+ export function getServerSchemaProvider() {
110
+ if (!initPromise) {
111
+ initPromise = ServerSchemaProvider.getInstance();
112
+ }
113
+ return initPromise;
114
+ }
115
+ //# sourceMappingURL=ServerSchemaProvider.js.map
@@ -0,0 +1,103 @@
1
+ .details-widget {
2
+ max-height: 0;
3
+ overflow: hidden;
4
+ transition: max-height 0.3s ease-out;
5
+ }
6
+
7
+ .details-widget.expanded {
8
+ max-height: 2000px;
9
+ transition: max-height 0.5s ease-in;
10
+ }
11
+
12
+ .details-widget-content {
13
+ padding: 1rem;
14
+ background-color: #f8fafc;
15
+ border-top: 1px solid #e2e8f0;
16
+ }
17
+
18
+ /* Schema specific styles */
19
+ .schema-content {
20
+ font-family: system-ui, -apple-system, sans-serif;
21
+ }
22
+
23
+ .tool-section {
24
+ background-color: white;
25
+ border-radius: 0.5rem;
26
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
27
+ margin-bottom: 1.5rem;
28
+ transition: transform 0.2s;
29
+ }
30
+
31
+ .tool-section:hover {
32
+ transform: translateY(-2px);
33
+ }
34
+
35
+ .property-item {
36
+ padding: 0.75rem;
37
+ margin: 0.5rem 0;
38
+ background-color: white;
39
+ border-radius: 0.375rem;
40
+ transition: all 0.2s;
41
+ }
42
+
43
+ .property-item:hover {
44
+ background-color: #f1f5f9;
45
+ }
46
+
47
+ .property-item.required {
48
+ border-left: 3px solid #3b82f6;
49
+ }
50
+
51
+ .property-name {
52
+ color: #1e40af;
53
+ font-weight: 600;
54
+ }
55
+
56
+ .property-type {
57
+ color: #64748b;
58
+ font-size: 0.875rem;
59
+ }
60
+
61
+ .property-desc {
62
+ color: #475569;
63
+ margin-top: 0.375rem;
64
+ line-height: 1.4;
65
+ }
66
+
67
+ .property-default {
68
+ color: #94a3b8;
69
+ font-size: 0.875rem;
70
+ font-family: monospace;
71
+ }
72
+
73
+ .required-badge {
74
+ background-color: #dbeafe;
75
+ color: #1e40af;
76
+ padding: 0.125rem 0.5rem;
77
+ border-radius: 9999px;
78
+ font-size: 0.75rem;
79
+ font-weight: 500;
80
+ }
81
+
82
+ .required-fields {
83
+ background-color: #fef3c7;
84
+ color: #92400e;
85
+ padding: 0.5rem;
86
+ border-radius: 0.375rem;
87
+ margin-bottom: 1rem;
88
+ font-size: 0.875rem;
89
+ }
90
+
91
+ .input-schema {
92
+ margin-top: 1rem;
93
+ padding: 1rem;
94
+ background-color: #f8fafc;
95
+ border-radius: 0.5rem;
96
+ border: 1px solid #e2e8f0;
97
+ }
98
+
99
+ .properties-list {
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: 0.75rem;
103
+ }
@@ -6,16 +6,14 @@
6
6
  }
7
7
 
8
8
  .server-item-content {
9
+ position: relative;
9
10
  border: 1px solid #e5e7eb;
10
11
  border-radius: 0.5rem;
11
12
  padding: 1rem;
13
+ padding-right: calc(120px + 3rem); /* Button width + spacing */
12
14
  margin-bottom: 1rem;
13
15
  background-color: #ffffff;
14
16
  transition: all 0.2s ease;
15
- position: relative;
16
- display: flex;
17
- justify-content: space-between;
18
- align-items: bottom; /* Center items vertically */
19
17
  }
20
18
 
21
19
  .server-item:hover .server-item-content {
@@ -59,40 +57,53 @@
59
57
  border-color: #3b82f6;
60
58
  }
61
59
 
62
- /* Server item content layout */
60
+ /* Server item layout */
63
61
  .server-item-info {
64
- flex: 1;
65
- padding-right: 1rem;
66
- min-width: 0; /* Prevent content from overflowing */
67
- }
68
-
69
- /* Install/Uninstall buttons positioning */
70
- .action-buttons {
71
- flex-shrink: 0;
72
- display: flex;
73
- align-items: center;
74
- margin-left: 1rem; /* Add some space between content and button */
62
+ width: 100%;
75
63
  }
76
64
 
77
- /* Ensure buttons stay in place on hover */
78
- .server-item:hover .action-buttons {
79
- position: relative;
80
- z-index: 2;
65
+ .server-item-header {
66
+ margin-bottom: 1rem;
81
67
  }
82
68
 
83
- /* Button styles */
84
- .action-buttons button {
85
- white-space: nowrap;
86
- padding: 0.5rem 1rem; /* Slightly larger padding for better visibility */
87
- min-width: 80px; /* Ensure consistent button width */
88
- text-align: center;
69
+ .server-item-header h5 {
70
+ margin-bottom: 0.5rem;
89
71
  }
90
72
 
91
- /* Status badges layout */
92
- .server-item-info .flex-wrap {
73
+ /* Client status section */
74
+ .flex-wrap {
93
75
  margin: -0.25rem; /* Negative margin to offset badge spacing */
94
76
  }
95
77
 
96
- .server-item-info .flex-wrap > * {
78
+ .flex-wrap > * {
97
79
  margin: 0.25rem; /* Even spacing between badges */
80
+ }
81
+
82
+ /* Install/Uninstall button section */
83
+ .action-buttons {
84
+ position: absolute;
85
+ right: 1rem;
86
+ top: 50%;
87
+ transform: translateY(-50%);
88
+ margin: 0;
89
+ }
90
+
91
+ .action-buttons button {
92
+ min-width: 120px;
93
+ padding: 0.5rem 1.5rem;
94
+ text-align: center;
95
+ font-weight: 600;
96
+ transition: all 0.15s ease;
97
+ white-space: nowrap;
98
+ }
99
+
100
+ /* Status badges */
101
+ .server-item-info .flex-wrap span {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ padding: 0.375rem 0.75rem;
105
+ border-radius: 9999px;
106
+ font-size: 0.75rem;
107
+ line-height: 1;
108
+ white-space: nowrap;
98
109
  }
@@ -15,12 +15,101 @@ export class DetailsWidget {
15
15
  this.container.appendChild(this.widgetElement);
16
16
  }
17
17
 
18
- setContent(description) {
19
- this.contentElement.innerHTML = `
20
- <div class="description-text">
21
- ${description || 'No description available.'}
22
- </div>
23
- `;
18
+ setContent(content) {
19
+ if (typeof content === 'string') {
20
+ this.contentElement.innerHTML = `
21
+ <div class="description-text">
22
+ ${content || 'No description available.'}
23
+ </div>
24
+ `;
25
+ return;
26
+ }
27
+
28
+ // If content is a schema object
29
+ if (content.schema) {
30
+ this.contentElement.innerHTML = this.renderSchema(content.schema);
31
+ }
32
+ }
33
+
34
+ renderSchema(schema) {
35
+ if (!schema || typeof schema !== 'object') {
36
+ return '<p>Invalid schema format</p>';
37
+ }
38
+
39
+ // The schema object from the API is wrapped in a "schema" property
40
+ const schemaContent = schema.schema || schema;
41
+
42
+ let html = '<div class="schema-content p-4">';
43
+
44
+ // Iterate through each tool in schema
45
+ Object.entries(schemaContent).forEach(([toolName, toolInfo]) => {
46
+ if (!toolInfo) return; // Skip if tool info is undefined
47
+
48
+ html += `
49
+ <div class="tool-section mb-6 border-b border-gray-200 pb-4">
50
+ <h3 class="text-lg font-semibold text-blue-600 mb-2">${toolInfo.name || toolName}</h3>
51
+ <p class="text-gray-700 mb-4">${toolInfo.description || 'No description available'}</p>
52
+
53
+ <div class="input-schema bg-gray-50 p-4 rounded-lg">
54
+ <h4 class="text-md font-semibold text-gray-700 mb-2">Input Schema</h4>
55
+ ${this.renderInputSchema(toolInfo.inputSchema)}
56
+ </div>
57
+ </div>
58
+ `;
59
+ });
60
+
61
+ html += '</div>';
62
+ return html;
63
+ }
64
+
65
+ renderInputSchema(schema) {
66
+ if (!schema || typeof schema !== 'object') {
67
+ return '<p>No input schema available</p>';
68
+ }
69
+
70
+ // Handle case where schema might be empty or have no properties
71
+ if (!schema.properties || Object.keys(schema.properties).length === 0) {
72
+ return '<p>No input properties defined</p>';
73
+ }
74
+
75
+ let html = '<div class="properties-list">';
76
+
77
+ try {
78
+ // Required fields banner if any
79
+ if (schema.required && Array.isArray(schema.required) && schema.required.length > 0) {
80
+ html += `
81
+ <div class="required-fields mb-3 bg-yellow-50 p-2 rounded">
82
+ <span class="text-sm font-medium text-yellow-800">Required fields: ${schema.required.join(', ')}</span>
83
+ </div>
84
+ `;
85
+ }
86
+
87
+ // Render each property
88
+ Object.entries(schema.properties).forEach(([propName, propDetails]) => {
89
+ if (!propDetails) return; // Skip if property details are undefined
90
+
91
+ const isRequired = schema.required && Array.isArray(schema.required) && schema.required.includes(propName);
92
+ const type = propDetails.type || 'any';
93
+
94
+ html += `
95
+ <div class="property-item mb-3 ${isRequired ? 'required' : ''} border-l-2 ${isRequired ? 'border-blue-500' : 'border-gray-300'} pl-3">
96
+ <div class="property-header flex items-center gap-2">
97
+ <span class="property-name font-medium text-gray-900">${this.escapeHtml(propName)}</span>
98
+ <span class="property-type text-sm text-gray-500">(${this.escapeHtml(type)})</span>
99
+ ${isRequired ? '<span class="required-badge text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded">Required</span>' : ''}
100
+ </div>
101
+ ${propDetails.description ? `<p class="property-desc text-sm text-gray-600 mt-1">${this.escapeHtml(propDetails.description)}</p>` : ''}
102
+ ${propDetails.default !== undefined ? `<p class="property-default text-sm text-gray-500 mt-1">Default: ${this.escapeHtml(JSON.stringify(propDetails.default))}</p>` : ''}
103
+ </div>
104
+ `;
105
+ });
106
+
107
+ html += '</div>';
108
+ return html;
109
+ } catch (error) {
110
+ console.error('Error rendering input schema:', error);
111
+ return '<p>Error rendering input schema</p>';
112
+ }
24
113
  }
25
114
 
26
115
  toggle() {
@@ -37,6 +126,18 @@ export class DetailsWidget {
37
126
  this.container.querySelector('.server-item-content').classList.add('expanded');
38
127
  }
39
128
 
129
+ escapeHtml(unsafe) {
130
+ if (unsafe === undefined || unsafe === null) {
131
+ return '';
132
+ }
133
+ return String(unsafe)
134
+ .replace(/&/g, "&amp;")
135
+ .replace(/</g, "&lt;")
136
+ .replace(/>/g, "&gt;")
137
+ .replace(/"/g, "&quot;")
138
+ .replace(/'/g, "&#039;");
139
+ }
140
+
40
141
  collapse() {
41
142
  this.widgetElement.classList.remove('expanded');
42
143
  this.container.querySelector('.server-item-content').classList.remove('expanded');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imcp",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Node.js SDK for Model Context Protocol (MCP)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,21 +8,22 @@ export function createServeCommand(): Command {
8
8
  return new Command('serve')
9
9
  .description('Serve local web interface')
10
10
  .option('-p, --port <port>', 'Port to run the server on', '3000')
11
+ .option('-f, --feed-file <filepath>', 'Path to a custom feed configuration file')
11
12
  .action(async (options) => {
12
13
  try {
13
14
  // Sync feeds before start the local UI
14
15
  await mcpManager.syncFeeds();
15
16
 
16
17
  // Ensure MCP manager is initialized before starting the web server
17
- await mcpManager.initialize();
18
-
18
+ await mcpManager.initialize(options.feedFile);
19
+
19
20
  const port = parseInt(options.port, 10);
20
21
  if (isNaN(port) || port < 1 || port > 65535) {
21
22
  throw new Error('Invalid port number');
22
23
  }
23
24
 
24
25
  await startWebServer(port);
25
-
26
+
26
27
  // The server is running, keep the process alive
27
28
  process.on('SIGINT', () => {
28
29
  console.log('\nShutting down server...');
package/src/cli/index.ts CHANGED
@@ -42,10 +42,10 @@ async function main(): Promise<void> {
42
42
 
43
43
  // Add all commands
44
44
  program.addCommand(createServeCommand());
45
- program.addCommand(createListCommand());
46
- program.addCommand(createInstallCommand());
45
+ // program.addCommand(createListCommand());
46
+ // program.addCommand(createInstallCommand());
47
47
  // program.addCommand(createUninstallCommand());
48
- program.addCommand(createPullCommand());
48
+ // program.addCommand(createPullCommand());
49
49
 
50
50
  // Error handling for the entire CLI
51
51
  program.exitOverride();
@@ -177,25 +177,46 @@ export class ConfigurationLoader {
177
177
  /**
178
178
  * Loads feed configurations into the MCP configuration
179
179
  */
180
- static async loadFeedsIntoConfiguration(configuration: MCPConfiguration): Promise<MCPConfiguration> {
180
+ static async loadFeedsIntoConfiguration(configuration: MCPConfiguration, feedFile?: string): Promise<MCPConfiguration> {
181
181
  try {
182
182
  await fs.mkdir(LOCAL_FEEDS_DIR, { recursive: true });
183
+ const feeds: Record<string, FeedConfiguration> = {};
184
+
185
+ // Load provided feed file if specified
186
+ if (feedFile) {
187
+ try {
188
+ const content = await fs.readFile(feedFile, 'utf8');
189
+ const config = JSON.parse(content) as FeedConfiguration;
190
+ if (config && config.name) {
191
+ feeds[config.name] = config;
192
+ console.log(`Loaded feed configuration from provided file: ${feedFile}`);
193
+ }
194
+ } catch (error) {
195
+ console.log(`Error loading feed configuration from provided file ${feedFile}:`, error);
196
+ }
197
+ }
198
+
199
+ // Load feeds from LOCAL_FEEDS_DIR
183
200
  const files = await fs.readdir(LOCAL_FEEDS_DIR);
184
201
  const jsonFiles = files.filter(file => file.endsWith('.json'));
185
202
 
186
- if (jsonFiles.length === 0) {
203
+ if (jsonFiles.length === 0 && !feedFile) {
187
204
  console.log(`No feed configuration files found in ${LOCAL_FEEDS_DIR}`);
188
205
  return configuration;
189
206
  }
190
207
 
191
- const feeds: Record<string, FeedConfiguration> = {};
192
208
  for (const file of jsonFiles) {
193
209
  try {
194
210
  const filePath = path.join(LOCAL_FEEDS_DIR, file);
195
211
  const content = await fs.readFile(filePath, 'utf8');
196
212
  const config = JSON.parse(content) as FeedConfiguration;
197
213
  if (config && config.name) {
198
- feeds[config.name] = config;
214
+ // If feed exists from provided file, skip the local one
215
+ if (!feeds[config.name]) {
216
+ feeds[config.name] = config;
217
+ } else {
218
+ console.log(`Skipping local feed ${config.name} as it was provided via --feed-file`);
219
+ }
199
220
  }
200
221
  } catch (error) {
201
222
  console.warn(`Error loading feed configuration from ${file}:`, error);
@@ -57,7 +57,7 @@ export class ConfigurationProvider {
57
57
  }
58
58
  }
59
59
 
60
- async initialize(): Promise<void> {
60
+ async initialize(feedFile?: string): Promise<void> {
61
61
  await this.withLock(async () => {
62
62
  const configDir = path.dirname(this.configPath);
63
63
  await fs.mkdir(configDir, { recursive: true });
@@ -75,7 +75,7 @@ export class ConfigurationProvider {
75
75
  }
76
76
 
77
77
  // Always load feeds and client settings, whether file existed or not
78
- await this.loadFeedsIntoConfiguration();
78
+ await this.loadFeedsIntoConfiguration(feedFile);
79
79
  await this.loadClientMCPSettings();
80
80
  } catch (error) {
81
81
  Logger.error('Error during initialization', error);
@@ -340,8 +340,8 @@ export class ConfigurationProvider {
340
340
  });
341
341
  }
342
342
 
343
- private async loadFeedsIntoConfiguration(): Promise<void> {
344
- this.configuration = await ConfigurationLoader.loadFeedsIntoConfiguration(this.configuration);
343
+ private async loadFeedsIntoConfiguration(feedFile?: string): Promise<void> {
344
+ this.configuration = await ConfigurationLoader.loadFeedsIntoConfiguration(this.configuration, feedFile);
345
345
  await this.saveConfiguration();
346
346
  }
347
347
 
@@ -9,8 +9,6 @@ import {
9
9
  ServerCategoryListOptions,
10
10
  ServerOperationResult,
11
11
  ServerUninstallOptions,
12
- InstallationStatus,
13
- UpdateRequirementOptions
14
12
  } from './types.js';
15
13
  import path from 'path';
16
14
 
@@ -28,9 +26,9 @@ export class MCPManager extends EventEmitter {
28
26
  await this.configProvider.syncFeeds();
29
27
  }
30
28
 
31
- async initialize(): Promise<void> {
29
+ async initialize(feedFile?: string): Promise<void> {
32
30
  try {
33
- await this.configProvider.initialize();
31
+ await this.configProvider.initialize(feedFile);
34
32
  } catch (error) {
35
33
  console.error("Error during MCPManager initialization:", error);
36
34
  throw error;