
mcp self contained client
无需外部服务器即可动态管理工具、提示和资源的内存内 MCP 客户端。
Repository Info
About This Server
无需外部服务器即可动态管理工具、提示和资源的内存内 MCP 客户端。
Model Context Protocol (MCP) - This server can be integrated with AI applications to provide additional context and capabilities, enabling enhanced AI interactions and functionality.
Documentation
MCPSelfContainedClient
A dynamic, in-memory implementation of a Model Context Protocol (MCP) client that allows you to define and manage tools, prompts, and resources locally—without connecting to an external server.
This client fully implements the MCP contract from the client's perspective, enabling:
- 🔧 Tool execution with argument validation and error handling
- 💬 Prompt generation with structured messages
- 🗂️ Resource management with URI template matching
- 📡 Resource subscriptions with real-time notifications
- 🔌 Transport support for client-server communication
- ✅ Full TypeScript support with JSDoc annotations
- 🔍 Introspection capabilities for all registered items
✨ Features
- ✅ No server required – works entirely in-process
- 🔧 Dynamically add/remove tools, prompts, and resources
- 🧪 Optional validation via Zod schemas
- 🔄 Streamable tool outputs (via async generators)
- 🧠 Comprehensive introspection and listing methods
- 🗂️ Namespace-based organization and bulk deletion
- 🛡️ Error handling that returns MCP-compliant error results
📦 Installation
This module is self-contained. Just copy index.mjs and types.d.ts into your project.
If you're using Zod for validation:
npm install zod
🛠️ Usage
Create a Client
import { MCPSelfContainedClient } from "./index.mjs";
const client = new MCPSelfContainedClient({
name: "my-mcp-client",
version: "1.0.0",
});
Declarative Configuration
You can also create a client with all capabilities defined declaratively, similar to mcp-declarative-server:
const client = new MCPSelfContainedClient(
{
name: "my-mcp-client",
version: "1.0.0",
},
{
tools: [
// Simple tool: [name, handler]
[
"greeting",
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }],
}),
],
// With schema: [name, schema, handler]
[
"calculate",
{ a: "number", b: "number" },
async ({ a, b }) => ({
content: [{ type: "text", text: `Sum: ${a + b}` }],
}),
],
// With Zod schema and description: [name, schema, handler, description]
[
"divide",
z.object({
dividend: z.number(),
divisor: z.number().refine((n) => n !== 0, "Cannot divide by zero"),
}),
async ({ dividend, divisor }) => ({
content: [{ type: "text", text: `Result: ${dividend / divisor}` }],
}),
"Divides two numbers",
],
],
prompts: [
// Simple prompt: [name, handler]
[
"welcome",
({ name }) => ({
messages: [
{
role: "user",
content: { type: "text", text: `Welcome, ${name}!` },
},
],
}),
],
// With schema and description: [name, schema, handler, description]
[
"code-review",
{ language: "string", code: "string" },
({ language, code }) => ({
messages: [
{
role: "system",
content: { type: "text", text: `You are a ${language} expert.` },
},
{
role: "user",
content: { type: "text", text: `Review this code:\n${code}` },
},
],
}),
"Generate a code review prompt",
],
],
resources: [
// Simple resource: [name, uriPattern, handler]
[
"docs",
"docs://{page}",
async (uri, { page }) => ({
contents: [
{
uri: uri.href,
text: `Documentation for ${page}`,
},
],
}),
],
// With options: [name, uriPattern, handler, options]
[
"api",
"api://{endpoint}",
async (uri, { endpoint }) => ({
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({ endpoint, data: "..." }),
},
],
}),
{ description: "API endpoint data", mimeType: "application/json" },
],
],
}
);
You can mix declarative and imperative registration - tools, prompts, and resources added via the declarative config work exactly the same as those added via the imperative API.
Tools
Tools are functions that can be called with arguments and return content.
Register a Simple Tool
client.tool("echo", async ({ message }) => ({
content: [{ type: "text", text: `Echo: ${message}` }],
}));
Register a Tool with Description
client.tool(
"calculate",
{ description: "Performs basic calculations" },
async ({ operation, a, b }) => {
const result = operation === "add" ? a + b : a - b;
return {
content: [{ type: "text", text: `Result: ${result}` }],
};
}
);
Register a Tool with Zod Validation
import { z } from "zod";
client.tool(
"divide",
z.object({
dividend: z.number(),
divisor: z.number().refine((n) => n !== 0, "Cannot divide by zero"),
}),
async ({ dividend, divisor }) => ({
content: [{ type: "text", text: `${dividend / divisor}` }],
})
);
Call a Tool
const result = await client.callTool({
name: "echo",
arguments: { message: "Hello, MCP!" },
});
// result: { content: [{ type: "text", text: "Echo: Hello, MCP!" }] }
List Tools
const { tools } = await client.listTools();
// tools: [{ name: "echo" }, { name: "calculate", description: "Performs basic calculations" }]
Handle Tool Errors
Tools that throw errors or have validation failures return error results:
const result = await client.callTool({
name: "divide",
arguments: { dividend: 10, divisor: 0 },
});
// result: { content: [{ type: "text", text: "Cannot divide by zero" }], isError: true }
Prompts
Prompts generate structured message templates based on arguments.
Register a Simple Prompt
client.prompt("greeting", ({ name }) => ({
messages: [
{
role: "user",
content: { type: "text", text: `Hello, ${name}!` },
},
],
}));
Register a Prompt with Metadata
client.prompt(
"code-review",
{
description: "Generate a code review request",
arguments: [
{ name: "language", description: "Programming language", required: true },
{ name: "code", description: "Code to review", required: true },
],
},
({ language, code }) => ({
messages: [
{
role: "system",
content: {
type: "text",
text: `You are a ${language} code reviewer.`,
},
},
{
role: "user",
content: {
type: "text",
text: `Please review this code:\n\n${code}`,
},
},
],
})
);
Get a Prompt
const result = await client.getPrompt({
name: "greeting",
arguments: { name: "Alice" },
});
// result: { messages: [{ role: "user", content: { type: "text", text: "Hello, Alice!" } }] }
List Prompts
const { prompts } = await client.listPrompts();
// prompts: [{ name: "greeting" }, { name: "code-review", description: "Generate a code review request", arguments: [...] }]
Resources
Resources provide access to content via URI patterns.
Register a Simple Resource
import { ResourceTemplate } from "./index.mjs";
client.resource(
"file-content",
new ResourceTemplate("file://{path}"),
async (uri, { path }) => ({
contents: [
{
uri: uri.href,
text: `Contents of ${path}`,
},
],
})
);
Register a Resource with Metadata
client.resource(
"api-docs",
new ResourceTemplate("docs://{section}/{page}"),
{
description: "API documentation pages",
mimeType: "text/markdown",
},
async (uri, { section, page }) => ({
contents: [
{
uri: uri.href,
mimeType: "text/markdown",
text: `# ${section} - ${page}\n\nDocumentation content here...`,
},
],
})
);
Read a Resource
const result = await client.readResource({
uri: "file://readme.txt",
});
// result: { contents: [{ uri: "file://readme.txt", text: "Contents of readme.txt" }] }
Resource Subscriptions
Resources can have watchers that monitor changes and notify subscribers:
// Register a resource with a watcher
let temperature = 20;
client.resource(
"temperature",
"sensor://temperature",
async () => ({
contents: [{
uri: "sensor://temperature",
mimeType: "application/json",
text: JSON.stringify({ value: temperature, unit: "celsius" })
}]
}),
// Watcher function
(uri, notifyChange) => {
const interval = setInterval(() => {
temperature += (Math.random() - 0.5) * 2; // Simulate temperature changes
notifyChange(); // Notify subscribers of the change
}, 1000);
// Return cleanup function
return () => clearInterval(interval);
}
);
// Subscribe to resource changes
const { subscriberId } = await client.subscribeToResource({
uri: "sensor://temperature"
});
// Listen for notifications
client.on('notification', (notification) => {
if (notification.method === 'notifications/resources/updated') {
console.log(`Resource updated: ${notification.params.uri}`);
}
});
// Unsubscribe when done
await client.unsubscribeFromResource({ uri: "sensor://temperature", subscriberId });
List Resources and Templates
// List all resources
const { resources } = await client.listResources();
// resources: [{ uri: "file://{path}", name: "file-content" }]
// List resource templates with full details
const { resourceTemplates } = await client.listResourceTemplates();
// resourceTemplates: [{ uriTemplate: "docs://{section}/{page}", name: "api-docs", description: "API documentation pages", mimeType: "text/markdown" }]
Resource with Binary Content
Resources can return base64-encoded binary data:
client.resource(
"image",
new ResourceTemplate("image://{name}"),
async (uri, { name }) => ({
contents: [
{
uri: uri.href,
mimeType: "image/png",
blob: Buffer.from(imageData).toString("base64"),
},
],
})
);
Advanced Features
Streaming Tool Output
Tools can return async generators for streaming content:
client.tool("count", async function* ({ to }) {
for (let i = 1; i <= to; i++) {
yield { type: "text", text: `Count: ${i}\n` };
await new Promise((resolve) => setTimeout(resolve, 100));
}
});
const result = await client.callTool({
name: "count",
arguments: { to: 5 },
});
// result.content will contain all yielded items
Namespace Management
Organize tools, prompts, and resources using namespaces:
// Register namespaced items
client.tool("math__add", async ({ a, b }) => ({
content: [{ type: "text", text: `${a + b}` }],
}));
client.tool("math__multiply", async ({ a, b }) => ({
content: [{ type: "text", text: `${a * b}` }],
}));
client.tool("string:reverse", async ({ text }) => ({
content: [{ type: "text", text: text.split("").reverse().join("") }],
}));
// Delete entire namespace
client.deleteNamespace("math"); // Removes math__add and math__multiply
Server Capabilities
Get information about what the client supports:
const capabilities = client.getServerCapabilities();
// {
// tools: {},
// prompts: {},
// resources: { subscribe: true, listChanged: true }
// }
Error Handling
The client provides robust error handling:
- Tool not found: Throws an error
- Validation errors: Returns error result with
isError: true - Handler errors: Returns error result with error message
- Invalid return format: Throws an error
Example error handling:
try {
const result = await client.callTool({
name: "nonexistent",
arguments: {},
});
} catch (error) {
console.error("Tool not found:", error.message);
}
// Validation error returns error result
const result = await client.callTool({
name: "divide",
arguments: { dividend: "not a number", divisor: 2 },
});
if (result.isError) {
console.error("Tool error:", result.content[0].text);
}
🔎 API Reference
Constructor
new MCPSelfContainedClient(
options: {
name: string;
version: string;
},
declarativeConfig?: {
tools?: Array<[name, handler] | [name, schema, handler] | [name, schema, handler, description]>;
prompts?: Array<[name, handler] | [name, schema, handler] | [name, schema, handler, description]>;
resources?: Array<[name, uriPattern, handler] | [name, uriPattern, handler, options] | [name, uriPattern, schema, handler, options]>;
}
)
Tool Methods
tool(name: string, handler: ToolHandler): voidtool(name: string, options: { description?: string }, handler: ToolHandler): voidtool(name: string, schema: ZodSchema, handler: ToolHandler): voidlistTools(): Promise<{ tools: Tool[] }>callTool(params: { name: string; arguments?: any }): Promise<ToolResult>deleteTool(name: string): void
Prompt Methods
prompt(name: string, handler: PromptHandler): voidprompt(name: string, options: { description?: string; arguments?: PromptArgument[] }, handler: PromptHandler): voidprompt(name: string, schema: ZodSchema, handler: PromptHandler): voidlistPrompts(): Promise<{ prompts: Prompt[] }>getPrompt(params: { name: string; arguments?: any }): Promise<GetPromptResult>deletePrompt(name: string): void
Resource Methods
resource(name: string, uriTemplate: ResourceTemplate, handler: ResourceHandler): voidresource(name: string, uriTemplate: ResourceTemplate, options: { description?: string; mimeType?: string }, handler: ResourceHandler): voidresource(name: string, uriTemplate: ResourceTemplate, schema: ZodSchema, handler: ResourceHandler): voidlistResources(): Promise<{ resources: Resource[] }>listResourceTemplates(): Promise<{ resourceTemplates: Array }>readResource(params: { uri: string }): Promise<ReadResourceResult>deleteResource(name: string): void
Utility Methods
deleteNamespace(prefix: string): voidgetServerCapabilities(): ServerCapabilities
📁 Files
index.mjs: Core JavaScript implementation with JSDoc type annotationstypes.d.ts: TypeScript type definitions for full type safety
🧠 Why Use This?
This is ideal for:
- Testing MCP-compatible applications without a backend
- Prototyping LLM toolchains or local pipelines
- Embedding MCP functionality in constrained environments
- Building mock MCP servers for development
- Creating dynamic tool/prompt/resource systems
🔌 Transport Support
The client can now act as an MCP server by connecting to a transport:
import { MCPSelfContainedClient } from "./index.mjs";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// Create server
const server = new MCPSelfContainedClient({
name: "my-server",
version: "1.0.0"
});
// Add capabilities
server.tool("greet", async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }]
}));
// Connect to transport
const transport = new StdioServerTransport();
await server.connect(transport);
The client supports the full MCP protocol including:
- Initialize/initialized handshake
- Tool, prompt, and resource operations
- Resource subscriptions with notifications
- Error handling and transport lifecycle
See demo/transport-demo.mjs for a complete example using the SDK's InMemoryTransport for testing.
🧩 Future Ideas
- Implement request/response logging
- Add middleware support for cross-cutting concerns
- Support for custom content types beyond text/image
📜 License
MIT License (or whatever suits your use case)
Quick Start
Clone the repository
git clone https://github.com/johnhenry/mcp-self-contained-clientInstall dependencies
cd mcp-self-contained-client
npm installFollow the documentation
Check the repository's README.md file for specific installation and usage instructions.
Repository Details
Recommended MCP Servers
Discord MCP
Enable AI assistants to seamlessly interact with Discord servers, channels, and messages.
Knit MCP
Connect AI agents to 200+ SaaS applications and automate workflows.
Apify MCP Server
Deploy and interact with Apify actors for web scraping and data extraction.
BrowserStack MCP
BrowserStack MCP Server for automated testing across multiple browsers.
Zapier MCP
A Zapier server that provides automation capabilities for various apps.