Tired of MCP clients bogged down by dependencies, complex OOP structures, and Node.js legacy? MCP Client Plugin is the antidote.
Built from the ground up with a Bun-first, zero-dependency, functional programming mindset, this plugin provides a refreshingly modern and efficient way to interact with Model Context Protocol servers and manage their processes seamlessly.
This plugin stands out by offering:
- 💧 Pure Functional & Immutable Core: Predictable state management, easier reasoning, and pure functions eliminate side effects common in OOP. No classes, no
this
. - 🚫 Truly Zero Runtime Dependencies: Relies exclusively on built-in Bun APIs (process spawning, fetch/SSE, streams). No
node_modules
baggage for core functionality. - 🔥 Bun-Native Performance & Mindset: Designed and optimized for Bun's blazing-fast runtime, leveraging its speed and integrated tooling.
- ⚙️ Integrated Server Process Management: Fluently start, stop, communicate with, and manage MCP server lifecycles (Stdio transport) directly within the client manager API. No separate manager needed!
- 🌐 Multi-Transport Support: Seamlessly handle both
stdio
(for local processes) andsse
(HTTP Server-Sent Events) transports. - ⛓️ Fluent, Chainable API: Inspired by ElysiaJS, manage servers and access client APIs with elegant method chaining (
.use()
). - 🔒 Type-Safe by Design: Strong TypeScript definitions throughout, avoiding
any
orunknown
for robust development. - 💨 Lightweight & Direct: Minimal abstraction layers between your code and the MCP communication.
- 🎯 Exceptional DX: Strongly typed, predictable API designed to get you productive fast.
Feature | ✨ MCP Client Plugin | Typical SDK Wrappers | Foundational Clients / SDKs |
---|---|---|---|
Dependencies | ✨ Zero Runtime! | SDK + Helpers (Zod, etc.) | SDK Core / Specific Libs |
Paradigm | 💧 Pure Functional | Often OOP/Mixed | Often OOP/Class-based |
State Management | 🧊 Immutable | Internal/Mutable | Internal/Mutable |
Process Management | ✅ Integrated (Stdio) | Usually Separate | Usually Separate / Manual |
Transport Handling | 🚀 Bun Native (Stdio/SSE) | Relies on SDK/Node APIs | Relies on SDK/Node APIs |
API Style | 🔗 Fluent Chaining | Method-based | Method-based |
Runtime | 🔥 Bun Optimized | Node.js (May work in Bun) | Node.js (May work in Bun) |
Bundle Size | 🤏 Tiny | Small -> Medium | Medium |
bun add mcp-client-plugin
# or
npm install mcp-client-plugin
# or
yarn add mcp-client-plugin
Get up and running in seconds!
// example.ts
import { manager } from 'mcp-client-plugin'; // Assuming installed package name
import type { ManagerConfig, NotificationHandler } from 'mcp-client-plugin';
// 1. Define your server configurations
const config: ManagerConfig = {
// An MCP server communicating via standard I/O
memoryServer: {
transport: {
type: 'stdio',
command: 'bunx', // Use bunx to run package executables
args: ['@modelcontextprotocol/server-memory'], // Example command
// env: { API_KEY: '...' } // Optional environment variables
},
},
// Add another server (e.g., filesystem)
fileSystem: {
transport: {
type: 'stdio',
command: 'bunx',
args: ['@modelcontextprotocol/server-filesystem', '.'], // Allow access to current dir
}
},
// Example SSE server (if you have one running)
// searchServer: {
// transport: {
// type: 'sse',
// url: 'http://localhost:3001/mcp', // URL for POST and SSE endpoint
// // headers: { 'Authorization': 'Bearer ...' } // Optional headers
// }
// }
};
// 2. Optional: Define a handler for server notifications
const handleNotifications: NotificationHandler = (serverName, notification) => {
console.log(`🔔 Notification [${serverName}]: ${notification.method}`, notification.params ?? '');
};
// 3. Initialize the manager and connect to servers using the chainable API
async function run() {
console.log('🚀 Initializing MCP Manager...');
try {
const mcpManager = await manager(config, { onNotification: handleNotifications })
.use('memoryServer') // Connect to the memory server
.use('fileSystem'); // Connect to the filesystem server
// .use('searchServer') // Connect to the SSE server
console.log('✅ Servers Connected & Ready!');
// 4. Get client APIs for specific servers
const memory = mcpManager.getClient('memoryServer');
const fs = mcpManager.getClient('fileSystem');
// 5. Interact with the servers!
if (memory) {
console.log('\n🧠 Querying Memory Server...');
const memTools = await memory.listTools();
console.log('Memory Tools:', memTools.map(t => t.name));
// Example: const result = await memory.callTool('createEntity', { ... });
}
if (fs) {
console.log('\n📁 Querying Filesystem Server...');
const fsResources = await fs.listResources();
console.log('FS Resources:', fsResources.map(r => r.uri));
// Example: const content = await fs.readResource('file:///./README.md');
// console.log('README content length:', content.length);
}
// ... Your agent logic here ...
// 6. Disconnect all servers when done
console.log('\n🔌 Disconnecting all servers...');
await mcpManager.disconnectAll();
console.log('✅ Disconnected.');
} catch (error) {
console.error('❌ MCP Manager Error:', error);
}
}
run();
Adding new MCP servers is straightforward. Just update the ManagerConfig
object passed to the manager
function.
The ManagerConfig
is an object where keys are your chosen server names (e.g., memoryServer
, myApiTool
) and values are ServerConfig
objects.
Each ServerConfig
requires a transport
property:
Use this for MCP servers running as local processes that communicate via stdin
/stdout
.
import type { ManagerConfig } from 'mcp-client-plugin';
const config: ManagerConfig = {
myStdioServer: {
transport: {
type: 'stdio',
command: 'bun', // The command to execute (e.g., 'bun', 'node', 'python', 'my_server_binary')
args: ['run', 'start-mcp-server.js', '--port', '8080'], // Arguments for the command
env: { // Optional: Environment variables for the process
API_KEY: Bun.env.MY_API_KEY,
LOG_LEVEL: 'debug',
},
cwd: '/path/to/server/working/directory', // Optional: Working directory
},
// requiredCapabilities: { ... } // Optional: Specify expected capabilities
},
// ... other servers
};
Use this for MCP servers accessible via HTTP, using Server-Sent Events for server-to-client communication and HTTP POST for client-to-server requests.
import type { ManagerConfig } from 'mcp-client-plugin';
const config: ManagerConfig = {
myRemoteServer: {
transport: {
type: 'sse',
// The *single* URL for both the SSE connection and POST requests
url: 'https://my-mcp-server.com/api/mcp',
headers: { // Optional: Headers for both SSE connection and POST requests
'Authorization': `Bearer ${Bun.env.REMOTE_API_TOKEN}`,
'X-Client-Version': '1.0.0',
},
},
// requiredCapabilities: { ... } // Optional
},
// ... other servers
};
- Initializes the manager.
config
: TheManagerConfig
object defining your servers.options
: Optional configuration:onNotification
:(serverName, notification) => void
- Callback for handling server-sent notifications.requestTimeoutMs
:number
- Default timeout for requests (default: 30000ms).
- Returns:
Promise<ManagerAPI>
- A promise resolving to the manager API object.
The object returned by manager()
and .use()
.
- .use(serverName: string):
- Connects to the specified server (spawns process if
stdio
). - Sends
initialize
request. - Returns:
Promise<ManagerAPI>
- A new ManagerAPI instance representing the updated state, allowing chaining.
- Connects to the specified server (spawns process if
- .getClient(serverName: string):
- Retrieves the
ClientAPI
for an already connected server. - Returns:
ClientAPI | undefined
.
- Retrieves the
- .getClientAsync(serverName: string):
- Retrieves the
ClientAPI
for an already connected server, waiting for pending connections. - Returns:
Promise<ClientAPI | undefined>
.
- Retrieves the
- .disconnectAll():
- Disconnects all active clients and terminates their associated processes/connections.
- Returns:
Promise<void>
.
- ._getState(): Internal use/debugging only. Returns the current immutable state object.
The object returned by manager.getClient()
. Contains methods to interact with a specific MCP server.
-
.getCapabilities()
- Returns the capabilities reported by the server during initialization.
- Returns:
Record<string, unknown> | undefined
-
.callTool(name, params, options?)
- Calls a tool on the server with optional progress tracking.
- Parameters:
name: string
- The name of the tool to call.params: Record<string, unknown>
- The parameters to pass to the tool.options?: { onProgress?: (progress: Progress) => void }
- Optional progress tracking callback.
- Returns:
Promise<TResult>
- A promise resolving to the tool result.
-
.listTools()
- Lists tools available on the server.
- Returns:
Promise<ReadonlyArray<Tool>>
- Array of available tools.
-
.readResource(uri)
- Reads a resource from the server, returning structured content.
- Parameters:
uri: string
- The URI of the resource to read.
- Returns:
Promise<ReadResourceResult>
- The resource content with metadata.
-
.listResources()
- Lists resources available on the server.
- Returns:
Promise<ReadonlyArray<Resource>>
- Array of available resources.
-
.listPrompts()
- Lists prompts available on the server.
- Returns:
Promise<ReadonlyArray<Prompt>>
- Array of available prompts.
-
.getPrompt(name, args?)
- Gets a prompt template with arguments filled in, returning structured messages.
- Parameters:
name: string
- The name of the prompt to get.args?: Record<string, unknown>
- Optional arguments to fill in the prompt template.
- Returns:
Promise<GetPromptResult>
- The prompt with filled arguments and message structure.
-
.listResourceTemplates()
- Lists resource templates available on the server.
- Parameters: None
- Returns:
Promise<ReadonlyArray<ResourceTemplate>>
- Array of available resource templates.
-
.complete(params)
- Completes an argument for a prompt, providing autocompletion suggestions.
- Parameters:
params: { ref: { type: string; name: string }; argument: { name: string; value: string } }
- Reference to the prompt and argument to complete.
- Returns:
Promise<CompleteResult>
- Completion suggestions as an array of values.
-
.setLoggingLevel(level)
- Sets the logging level for the server, controlling verbosity of log messages.
- Parameters:
level: LoggingLevel
- One of 'debug', 'info', 'warning', or 'error'.
- Returns:
Promise<void>
-
.ping()
- Checks connectivity with the server.
- Returns:
Promise<void>
-
.disconnect()
- Disconnects this specific client and terminates its server process/connection.
- Returns:
Promise<void>
(Refer to src/types.ts
for detailed parameter and return types)
Provide an onNotification
callback in the manager
options to react to notifications sent by servers (e.g., $/progress
, resource changes).
const handleNotifications: NotificationHandler = (serverName, notification) => {
if (notification.method === '$/progress') {
console.log(`Progress from ${serverName}:`, notification.params);
} else if (notification.method === 'notifications/message') {
console.log(`Log from ${serverName}:`, notification.params);
} else {
console.log(`Notification [${serverName}]: ${notification.method}`);
}
};
const mcpManager = await manager(config)
.use('memoryServer');
const memory = mcpManager.getClient('memoryServer');
if (memory) {
const result = await memory.callTool(
'longRunningTask',
{ input: 'some large data' },
{
onProgress: (progress) => {
console.log(`Progress: ${progress.progress}/${progress.total}`);
}
}
);
console.log('Task completed with result:', result);
}
const mcpManager = await manager(config)
.use('fileSystem');
const fs = mcpManager.getClient('fileSystem');
if (fs) {
// List available resource templates
const templates = await fs.listResourceTemplates();
console.log('Available templates:', templates);
// Read a resource using a templated URI
const logResource = await fs.readResource('file:///logs/app.log');
console.log('Log content:', logResource.contents[0].text);
}
const mcpManager = await manager(config)
.use('promptServer');
const promptClient = mcpManager.getClient('promptServer');
if (promptClient) {
// Get completion suggestions for a prompt argument
const completions = await promptClient.complete({
ref: { type: 'ref/prompt', name: 'countryPoem' },
argument: { name: 'name', value: 'germ' }
});
console.log('Suggested completions:', completions.completion.values);
// Output: Suggested completions: ['Germany']
// Use the completed value to get the prompt
const prompt = await promptClient.getPrompt('countryPoem', { name: 'Germany' });
console.log('Prompt messages:', prompt.messages);
}
const mcpManager = await manager(config, {
onNotification: (serverName, notification) => {
if (notification.method === 'notifications/message') {
const { level, message } = notification.params;
console.log(`[${level}] ${serverName}: ${message}`);
}
}
}).use('debugServer');
const debug = mcpManager.getClient('debugServer');
if (debug) {
// Set the logging level to receive more detailed logs
await debug.setLoggingLevel('debug');
// Now the server will send debug-level log messages
// which will be captured by the onNotification handler
}
- Requests automatically time out based on
requestTimeoutMs
in options. - Errors during connection, communication, or from the server will reject the corresponding promises (e.g.,
.use()
,.callTool()
). - Use standard
try...catch
blocks aroundawait
calls. - Unhandled transport or process errors are logged to the console. Implement robust error handling appropriate for your application.
Contributions are welcome! Feel free to open issues or submit Pull Requests.
- Fork the repository.
- Create your feature branch (
git checkout -b feature/AmazingFeature
). - Commit your changes (
git commit -m 'Add some AmazingFeature'
). - Push to the branch (
git push origin feature/AmazingFeature
). - Open a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.