Skip to content

Feature/loopbacktransport #367

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,6 @@ out

.DS_Store
dist/

# Ignoring IntelliJ files
.idea/
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [Running Your Server](#running-your-server)
- [stdio](#stdio)
- [Streamable HTTP](#streamable-http)
- [Loopback Transport](#loopback-transport-in-memory)
- [Testing and Debugging](#testing-and-debugging)
- [Examples](#examples)
- [Echo Server](#echo-server)
Expand Down Expand Up @@ -380,6 +381,46 @@ This stateless approach is useful for:
- RESTful scenarios where each request is independent
- Horizontally scaled deployments without shared session state

### Loopback Transport (In-Memory)

The Loopback Transport provides an efficient way to connect MCP clients and servers within the same execution context,
making it ideal for testing, browser-based applications, and environments where external network calls are unnecessary.
Unlike stdio or Streamable HTTP, Loopback Transport is intended exclusively for scenarios where clients and servers operate
in the same execution context. Note that the stdio transport cannot operate in a browser. While Streamable HTTP can be used
in a brower with a lot of complexity, Loopback Transport is a much simpler option.

#### Usage

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { LoopbackTransport } from "@modelcontextprotocol/sdk/shared/loopback.js";

// Create server and client
const server = new McpServer({ name: "example-server", version: "1.0.0" });
const client = new Client({ name: "example-client", version: "1.0.0" });

// Create loopback transports
const clientTransport = new LoopbackTransport({ name: "client" });
const serverTransport = new LoopbackTransport({ name: "server" });

// Connect transports in-memory
clientTransport.connect(serverTransport);

// Connect to MCP
await server.connect(serverTransport);
await client.connect(clientTransport);

// Use client and server as normal
```

#### When to Use Loopback Transport

- **Testing**: Provides fast, reliable tests without external dependencies.
- **Browser Applications**: Enables MCP functionality entirely within the browser.
- **Rapid Prototyping**: Ideal for quick iteration without networking overhead.


### Testing and Debugging

To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information.
Expand Down
123 changes: 123 additions & 0 deletions src/integration-tests/loopback.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import {JSONRPCRequest, ResultSchema} from "../types.js";
import {z} from "zod";
import {Server} from "../server/index.js";
import {Client} from "../client/index.js";
import {LoopbackTransport} from "../shared/loopback.js";

// Assume you have a base RequestSchema defined somewhere in your code.
// For this example, we create a minimal stub.
const RequestSchema = z.object({
jsonrpc: z.literal("2.0"),
id: z.union([z.number(), z.string()]),
});

// Define the weather request schema.
const GetWeatherRequestSchema = RequestSchema.extend({
method: z.literal("weather/get"),
params: z.object({
city: z.string(),
}),
});
const WeatherResultSchema = ResultSchema.extend({
temperature: z.number(),
conditions: z.string(),
});

describe("Client-Server Integration Test for Weather Request", () => {
let clientTransport: LoopbackTransport;
let serverTransport: LoopbackTransport
let client: Client;
let server: Server;

beforeEach(async () => {
clientTransport = new LoopbackTransport();
serverTransport = new LoopbackTransport();
clientTransport.connect(serverTransport)

server = new Server(
{name: "test server", version: "1.0"},
{
capabilities: {
prompts: {},
resources: {},
tools: {},
logging: {},
},
instructions: "Test instructions",
}
);
await server.connect(serverTransport);

client = new Client(
{name: "test client", version: "1.0"},
{capabilities: {sampling: {}}}
);

await client.connect(clientTransport);

// Register the weather request handler on the server.
server.setRequestHandler(GetWeatherRequestSchema, (_) => {
// In a real handler, you might use request.params.city.
return {
temperature: 72,
conditions: "sunny",
};
});
});

test("client sends weather/get request and receives expected response", async () => {
// Construct a JSONRPCRequest matching the weather schema.
const request: JSONRPCRequest = {
jsonrpc: "2.0",
id: 1,
method: "weather/get",
params: {city: "New York"},
};

// The client issues the request; we assume client.request returns a promise resolving to a JSONRPCResponse.
const response = await client.request(request, WeatherResultSchema);

// Verify that the response from the server matches the expected result.
expect(response).toEqual({temperature: 72, conditions: "sunny"});
});

test("server returns error response for invalid request", async () => {
server.setRequestHandler(GetWeatherRequestSchema, () => {
throw new Error("Unexpected server error");
});

const request: JSONRPCRequest = {
jsonrpc: "2.0",
id: 2,
method: "weather/get",
params: {city: "Invalid City"},
};

await expect(client.request(request, WeatherResultSchema)).rejects.toThrow();
});

test("multiple concurrent requests are handled correctly", async () => {
const cities = ["New York", "London", "Tokyo"];
server.setRequestHandler(GetWeatherRequestSchema, (req) => ({
temperature: 20,
conditions: `Weather for ${req.params.city}`,
}));

const responses = await Promise.all(
cities.map((city, idx) => {
const request: JSONRPCRequest = {
jsonrpc: "2.0",
id: idx + 1,
method: "weather/get",
params: {city},
};
return client.request(request, WeatherResultSchema);
})
);

responses.forEach((response, idx) => {
expect(response).toEqual({temperature: 20, conditions: `Weather for ${cities[idx]}`});
});
});

});
Loading