Skip to content

Commit 880440e

Browse files
authored
Merge pull request #2 from in-fun/feat/alias
Install using package alias
2 parents 8e4318d + 54b5343 commit 880440e

File tree

6 files changed

+381
-31
lines changed

6 files changed

+381
-31
lines changed

README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22

33
The CLI manager for MCP servers.
44

5+
## 🧩 MCP Manifest Concept
6+
7+
MCPBar implements a standardized approach for MCP servers using `mcp.json` manifest files. This solution:
8+
9+
- 📄 Uses a **standardized manifest format** (`mcp.json`) similar to `package.json` in npm
10+
- 🌐 Supports a **decentralized distribution model** where developers can host manifests anywhere
11+
- 🔄 Allows installation directly from URLs, local files, or package aliases
12+
- 🔍 Features an optional registry for enhanced discoverability
13+
- 🔐 Securely handles server configuration with explicit handling of sensitive inputs
14+
- 📚 Includes an **open registry** with 1500+ MCP servers in the `registry` directory
15+
16+
This approach simplifies discovery, installation, and configuration of MCP servers across different clients, making the MCP ecosystem more accessible and organized.
17+
18+
The extensive registry is the foundation of an open and standardized MCP ecosystem, enabling developers to easily discover, share, and contribute to the growing collection of MCP servers.
19+
20+
For more details, see the [MCP Manifest Proposal](./doc/proposal.md) and [Concept Overview](./doc/idea.md).
21+
522
## 🌟 Features
623

724
- 🔄 Simple installation and management of MCP servers
@@ -41,12 +58,14 @@ mcpbar s <query> # Short alias for search
4158

4259
### Install a MCP Server
4360

44-
Install a MCP server from a URL or file path:
61+
Install a MCP server from a URL (any protocol supported by fetch), file path, or package alias:
4562

4663
```bash
4764
mcpbar install <path-to-manifest.json> # Install from local manifest file
4865
mcpbar i <path-to-manifest.json> # Short alias for install
49-
mcpbar install https://example.com/manifest.json # Install from URL
66+
mcpbar install https://example.com/manifest.json # Install from HTTP URL
67+
mcpbar install file:///path/to/manifest.json # Install using file protocol
68+
mcpbar install vendor/package-name # Install using package alias
5069
```
5170

5271
Example:
@@ -55,8 +74,14 @@ Example:
5574
# Install from a local manifest file
5675
mcpbar install ./manifests/github.json
5776

77+
# Install using package alias (shorthand)
78+
mcpbar i 21st-dev/magic-mcp
79+
5880
# Install from a URL
59-
mcpbar install https://raw.githubusercontent.com/example/repo/main/mcp.json
81+
mcpbar install https://esm.sh/gh/in-fun/mcpbar/registry/21st-dev/magic-mcp.json
82+
83+
# Install using file protocol
84+
mcpbar install file:///Users/username/projects/manifests/custom.json
6085
```
6186

6287
### Remove a MCP Server

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mcpbar",
3-
"version": "0.0.1",
3+
"version": "0.1.0",
44
"description": "`mcpbar` is a package manager for MCP server.",
55
"bin": {
66
"mcpbar": "./bin/run"
@@ -13,7 +13,7 @@
1313
"dist",
1414
"bin"
1515
],
16-
"homepage": "https://mcp.bar",
16+
"homepage": "https://www.mcp.bar",
1717
"repository": {
1818
"type": "git",
1919
"url": "git+ssh://git@github.com/in-fun/mcpbar.git"

src/commands/install.test.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { handler } from './install'
2+
import { downloadManifest } from '../packages'
3+
import { readClientConfig } from '../config'
4+
import { logger } from '../logger'
5+
import { ArgumentsCamelCase } from 'yargs'
6+
7+
// Create an interface matching the structure of yargs arguments object
8+
interface InstallArgv {
9+
source: string
10+
name?: string
11+
client?: string
12+
}
13+
14+
// Mock dependencies
15+
jest.mock('../packages', () => ({
16+
downloadManifest: jest.fn(),
17+
}))
18+
19+
jest.mock('../config', () => ({
20+
readClientConfig: jest.fn(),
21+
writeClientConfig: jest.fn(),
22+
getAvailableClients: jest.fn().mockReturnValue(['claude']),
23+
clientPaths: { claude: '/path/to/claude/config' },
24+
createPlatformCommand: jest.fn().mockImplementation((command, args) => ({ command, args })),
25+
}))
26+
27+
jest.mock('../logger', () => ({
28+
logger: {
29+
error: jest.fn(),
30+
info: jest.fn(),
31+
success: jest.fn(),
32+
prompt: jest.fn(),
33+
},
34+
}))
35+
36+
// Create a helper function to generate a properly typed yargs args object
37+
function createMockArgs(args: Partial<InstallArgv>): ArgumentsCamelCase<InstallArgv> {
38+
return {
39+
...args,
40+
_: [],
41+
$0: 'mcpbar',
42+
} as ArgumentsCamelCase<InstallArgv>
43+
}
44+
45+
describe('Install Command', () => {
46+
beforeEach(() => {
47+
jest.clearAllMocks()
48+
49+
// Set up default mock returns
50+
const mockClientConfig = { mcpServers: {} }
51+
;(readClientConfig as jest.Mock).mockResolvedValue(mockClientConfig)
52+
53+
// Setup for downloadManifest
54+
;(downloadManifest as jest.Mock).mockResolvedValue({
55+
name: 'test-server',
56+
description: 'Test server description',
57+
manifest: {
58+
name: 'test-server',
59+
inputs: [],
60+
server: {
61+
command: 'echo',
62+
args: ['hello'],
63+
env: {},
64+
},
65+
},
66+
buildConfig: jest.fn().mockReturnValue({
67+
command: 'echo',
68+
args: ['hello'],
69+
env: {},
70+
}),
71+
})
72+
73+
// Setup for prompt
74+
;(logger.prompt as jest.Mock).mockResolvedValue(true)
75+
})
76+
77+
it('should transform package alias to registry URL', async () => {
78+
// Call the handler with an alias
79+
await handler(
80+
createMockArgs({
81+
source: 'vendor/package-name',
82+
client: 'claude',
83+
}),
84+
)
85+
86+
// Check if downloadManifest was called with the transformed URL
87+
expect(downloadManifest).toHaveBeenCalledWith('https://esm.sh/gh/in-fun/mcpbar/registry/vendor/package-name.json')
88+
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Using package alias'))
89+
})
90+
91+
it('should not transform URLs with protocols', async () => {
92+
// Call the handler with a URL
93+
await handler(
94+
createMockArgs({
95+
source: 'https://example.com/manifest.json',
96+
client: 'claude',
97+
}),
98+
)
99+
100+
// Should pass through the URL unchanged
101+
expect(downloadManifest).toHaveBeenCalledWith('https://example.com/manifest.json')
102+
expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Using package alias'))
103+
})
104+
105+
it('should not transform absolute file paths', async () => {
106+
// Call the handler with an absolute path
107+
await handler(
108+
createMockArgs({
109+
source: '/path/to/manifest.json',
110+
client: 'claude',
111+
}),
112+
)
113+
114+
// Should pass through the path unchanged
115+
expect(downloadManifest).toHaveBeenCalledWith('/path/to/manifest.json')
116+
expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Using package alias'))
117+
})
118+
119+
it('should not transform relative file paths', async () => {
120+
// Call the handler with a relative path
121+
await handler(
122+
createMockArgs({
123+
source: './manifests/package.json',
124+
client: 'claude',
125+
}),
126+
)
127+
128+
// Should pass through the path unchanged
129+
expect(downloadManifest).toHaveBeenCalledWith('./manifests/package.json')
130+
expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Using package alias'))
131+
})
132+
133+
it('should handle file:// protocol', async () => {
134+
// Call the handler with file protocol
135+
await handler(
136+
createMockArgs({
137+
source: 'file:///path/to/manifest.json',
138+
client: 'claude',
139+
}),
140+
)
141+
142+
// Should pass through the URL unchanged
143+
expect(downloadManifest).toHaveBeenCalledWith('file:///path/to/manifest.json')
144+
expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Using package alias'))
145+
})
146+
147+
it('should handle other protocols', async () => {
148+
// Call the handler with data protocol
149+
await handler(
150+
createMockArgs({
151+
source: 'data:application/json;base64,ewogICJuYW1lIjogInRlc3Qtc2VydmVyIgp9',
152+
client: 'claude',
153+
}),
154+
)
155+
156+
// Should pass through the URL unchanged
157+
expect(downloadManifest).toHaveBeenCalledWith('data:application/json;base64,ewogICJuYW1lIjogInRlc3Qtc2VydmVyIgp9')
158+
expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Using package alias'))
159+
})
160+
})

src/commands/install.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function builder(yargs: Argv): Argv<InstallArgv> {
1919
return yargs
2020
.positional('source', {
2121
type: 'string',
22-
description: 'URL or file path to the MCP server manifest',
22+
description: 'URL, file path, or package alias for the MCP server manifest',
2323
demandOption: true,
2424
})
2525
.option('name', {
@@ -48,8 +48,16 @@ export async function handler(argv: ArgumentsCamelCase<InstallArgv>) {
4848
return
4949
}
5050

51+
// Transform source if it's an alias (not a file path or URL)
52+
let source = manifestSource
53+
if (!source.startsWith('./') && !source.startsWith('/') && !source.includes('://') && !source.startsWith('data:')) {
54+
// It's an alias, transform to registry URL
55+
source = `https://esm.sh/gh/in-fun/mcpbar/registry/${source}.json`
56+
logger.info(`Using package alias: ${manifestSource}${source}`)
57+
}
58+
5159
// Download/load manifest from URL or file path
52-
const server = await downloadManifest(manifestSource)
60+
const server = await downloadManifest(source)
5361
if (!server) {
5462
return // Error already logged in downloadManifest
5563
}

0 commit comments

Comments
 (0)