Skip to content

Commit 968970b

Browse files
committed
stdio-native
1 parent 71c0f2b commit 968970b

File tree

4 files changed

+409
-83
lines changed

4 files changed

+409
-83
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="../../src/SuperSocket.Server/SuperSocket.Server.csproj" />
12+
<ProjectReference Include="../../src/SuperSocket.MCP/SuperSocket.MCP.csproj" />
13+
<ProjectReference Include="../../src/SuperSocket.Connection/SuperSocket.Connection.csproj" />
14+
<ProjectReference Include="../../src/SuperSocket.Server.Abstractions/SuperSocket.Server.Abstractions.csproj" />
15+
</ItemGroup>
16+
17+
</Project>

samples/McpStdioServer/Program.cs

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Extensions.Logging;
6+
using SuperSocket.MCP;
7+
using SuperSocket.MCP.Abstractions;
8+
using SuperSocket.MCP.Commands;
9+
using SuperSocket.MCP.Extensions;
10+
using SuperSocket.MCP.Models;
11+
using SuperSocket.Server;
12+
using SuperSocket.Server.Abstractions.Session;
13+
using SuperSocket.Server.Host;
14+
using SuperSocket.Command;
15+
16+
namespace McpStdioServer
17+
{
18+
/// <summary>
19+
/// MCP Server implementation that communicates over stdio (stdin/stdout)
20+
/// This is the standard way MCP servers are used - they communicate via stdio
21+
/// and are typically spawned by MCP clients as subprocess
22+
/// </summary>
23+
class Program
24+
{
25+
static async Task Main(string[] args)
26+
{
27+
// Disable console buffering for immediate I/O
28+
Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true });
29+
30+
var host = SuperSocketHostBuilder.Create<McpMessage, McpPipelineFilter>()
31+
.UseConsole() // Use stdio instead of TCP - this is the key change!
32+
.UseCommand<string, McpMessage>(commandOptions =>
33+
{
34+
// Register MCP commands
35+
commandOptions.AddCommand<InitializeCommand>();
36+
commandOptions.AddCommand<InitializedCommand>();
37+
commandOptions.AddCommand<ListToolsCommand>();
38+
commandOptions.AddCommand<CallToolCommand>();
39+
commandOptions.AddCommand<ListResourcesCommand>();
40+
commandOptions.AddCommand<ListPromptsCommand>();
41+
})
42+
.ConfigureServices((hostCtx, services) =>
43+
{
44+
// Register MCP services
45+
services.AddSingleton<IMcpHandlerRegistry, McpHandlerRegistry>();
46+
services.AddSingleton<McpServerInfo>(new McpServerInfo
47+
{
48+
Name = "SuperSocket MCP Stdio Server",
49+
Version = "1.0.0",
50+
ProtocolVersion = "2024-11-05"
51+
});
52+
53+
// Register sample tools
54+
services.AddSingleton<IMcpToolHandler, EchoToolHandler>();
55+
services.AddSingleton<IMcpToolHandler, MathToolHandler>();
56+
services.AddSingleton<IMcpToolHandler, FileReadToolHandler>();
57+
})
58+
.ConfigureLogging((hostCtx, loggingBuilder) =>
59+
{
60+
// Minimal logging to stderr to avoid interfering with MCP protocol on stdout
61+
loggingBuilder.AddConsole(options =>
62+
{
63+
options.LogToStandardErrorThreshold = LogLevel.Warning;
64+
});
65+
loggingBuilder.SetMinimumLevel(LogLevel.Warning);
66+
})
67+
.Build();
68+
69+
await host.RunAsync();
70+
}
71+
}
72+
73+
/// <summary>
74+
/// Simple echo tool that returns the input message
75+
/// </summary>
76+
public class EchoToolHandler : IMcpToolHandler
77+
{
78+
public Task<McpTool> GetToolDefinitionAsync()
79+
{
80+
return Task.FromResult(new McpTool
81+
{
82+
Name = "echo",
83+
Description = "Echo back the input message",
84+
InputSchema = new
85+
{
86+
type = "object",
87+
properties = new
88+
{
89+
message = new { type = "string", description = "Message to echo back" }
90+
},
91+
required = new[] { "message" }
92+
}
93+
});
94+
}
95+
96+
public Task<McpToolResult> ExecuteAsync(Dictionary<string, object> arguments)
97+
{
98+
var message = arguments.TryGetValue("message", out var msg) ? msg.ToString() : "Hello!";
99+
return Task.FromResult(new McpToolResult
100+
{
101+
Content = new List<McpContent>
102+
{
103+
new McpContent { Type = "text", Text = $"Echo: {message}" }
104+
}
105+
});
106+
}
107+
}
108+
109+
/// <summary>
110+
/// Math tool that performs basic arithmetic operations
111+
/// </summary>
112+
public class MathToolHandler : IMcpToolHandler
113+
{
114+
public Task<McpTool> GetToolDefinitionAsync()
115+
{
116+
return Task.FromResult(new McpTool
117+
{
118+
Name = "math",
119+
Description = "Perform basic math operations (add, subtract, multiply, divide)",
120+
InputSchema = new
121+
{
122+
type = "object",
123+
properties = new
124+
{
125+
operation = new { type = "string", description = "The operation to perform", @enum = new[] { "add", "subtract", "multiply", "divide" } },
126+
a = new { type = "number", description = "First number" },
127+
b = new { type = "number", description = "Second number" }
128+
},
129+
required = new[] { "operation", "a", "b" }
130+
}
131+
});
132+
}
133+
134+
public Task<McpToolResult> ExecuteAsync(Dictionary<string, object> arguments)
135+
{
136+
try
137+
{
138+
var operation = arguments.TryGetValue("operation", out var op) ? op.ToString() : "";
139+
var a = Convert.ToDouble(arguments.TryGetValue("a", out var aVal) ? aVal : 0);
140+
var b = Convert.ToDouble(arguments.TryGetValue("b", out var bVal) ? bVal : 0);
141+
142+
double result = operation switch
143+
{
144+
"add" => a + b,
145+
"subtract" => a - b,
146+
"multiply" => a * b,
147+
"divide" => b != 0 ? a / b : throw new DivideByZeroException("Cannot divide by zero"),
148+
_ => throw new ArgumentException($"Unknown operation: {operation}")
149+
};
150+
151+
return Task.FromResult(new McpToolResult
152+
{
153+
Content = new List<McpContent>
154+
{
155+
new McpContent { Type = "text", Text = $"Result: {a} {operation} {b} = {result}" }
156+
}
157+
});
158+
}
159+
catch (Exception ex)
160+
{
161+
return Task.FromResult(new McpToolResult
162+
{
163+
Content = new List<McpContent>
164+
{
165+
new McpContent { Type = "text", Text = $"Error: {ex.Message}" }
166+
},
167+
IsError = true
168+
});
169+
}
170+
}
171+
}
172+
173+
/// <summary>
174+
/// File reading tool for accessing local files
175+
/// </summary>
176+
public class FileReadToolHandler : IMcpToolHandler
177+
{
178+
public Task<McpTool> GetToolDefinitionAsync()
179+
{
180+
return Task.FromResult(new McpTool
181+
{
182+
Name = "read_file",
183+
Description = "Read the contents of a text file",
184+
InputSchema = new
185+
{
186+
type = "object",
187+
properties = new
188+
{
189+
path = new { type = "string", description = "Path to the file to read" }
190+
},
191+
required = new[] { "path" }
192+
}
193+
});
194+
}
195+
196+
public async Task<McpToolResult> ExecuteAsync(Dictionary<string, object> arguments)
197+
{
198+
try
199+
{
200+
var path = arguments.TryGetValue("path", out var p) ? p.ToString() : "";
201+
202+
if (string.IsNullOrEmpty(path))
203+
{
204+
throw new ArgumentException("File path is required");
205+
}
206+
207+
if (!File.Exists(path))
208+
{
209+
throw new FileNotFoundException($"File not found: {path}");
210+
}
211+
212+
var content = await File.ReadAllTextAsync(path);
213+
214+
return new McpToolResult
215+
{
216+
Content = new List<McpContent>
217+
{
218+
new McpContent { Type = "text", Text = content }
219+
}
220+
};
221+
}
222+
catch (Exception ex)
223+
{
224+
return new McpToolResult
225+
{
226+
Content = new List<McpContent>
227+
{
228+
new McpContent { Type = "text", Text = $"Error reading file: {ex.Message}" }
229+
},
230+
IsError = true
231+
};
232+
}
233+
}
234+
}
235+
}

samples/McpStdioServer/README.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# MCP Stdio Server Sample
2+
3+
This sample demonstrates how to create an MCP (Model Context Protocol) server that communicates over stdio (stdin/stdout) using SuperSocket's new console connection support.
4+
5+
## Overview
6+
7+
MCP servers are typically designed to run as subprocesses and communicate via stdio rather than TCP. This is the standard deployment pattern for MCP servers, where:
8+
9+
- MCP clients spawn the server as a subprocess
10+
- Communication happens via stdin/stdout using newline-delimited JSON-RPC messages
11+
- The server process is managed by the client
12+
13+
## Key Features
14+
15+
- **Stdio Communication**: Uses SuperSocket's new `UseConsole()` extension for stdio-based communication
16+
- **Line-based Protocol**: Updated `McpPipelineFilter` that processes newline-delimited JSON messages
17+
- **Command Architecture**: Uses SuperSocket's command system for proper MCP message handling
18+
- **Tool Implementation**: Includes sample tools (echo, math, file reading)
19+
20+
## Protocol Changes
21+
22+
### Before (TCP/HTTP style):
23+
```
24+
Content-Length: 123
25+
Content-Type: application/json
26+
27+
{"jsonrpc": "2.0", "method": "initialize", ...}
28+
```
29+
30+
### After (Stdio style):
31+
```
32+
{"jsonrpc": "2.0", "method": "initialize", ...}
33+
{"jsonrpc": "2.0", "method": "initialized"}
34+
```
35+
36+
## Available Tools
37+
38+
1. **Echo Tool** - Simple echo functionality
39+
```json
40+
{"method": "tools/call", "params": {"name": "echo", "arguments": {"message": "Hello!"}}}
41+
```
42+
43+
2. **Math Tool** - Basic arithmetic operations
44+
```json
45+
{"method": "tools/call", "params": {"name": "math", "arguments": {"operation": "add", "a": 5, "b": 3}}}
46+
```
47+
48+
3. **File Read Tool** - Read local text files
49+
```json
50+
{"method": "tools/call", "params": {"name": "read_file", "arguments": {"path": "/path/to/file.txt"}}}
51+
```
52+
53+
## Building and Running
54+
55+
```bash
56+
cd samples/McpStdioServer
57+
dotnet build
58+
dotnet run
59+
```
60+
61+
## Usage as MCP Server
62+
63+
### Direct Testing
64+
You can test the server directly by piping JSON messages:
65+
66+
```bash
67+
echo '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test", "version": "1.0"}}}' | dotnet run
68+
```
69+
70+
### Integration with MCP Clients
71+
72+
Configure your MCP client to spawn this server:
73+
74+
```json
75+
{
76+
"mcpServers": {
77+
"supersocket-mcp": {
78+
"command": "dotnet",
79+
"args": ["run", "--project", "/path/to/McpStdioServer"],
80+
"cwd": "/path/to/McpStdioServer"
81+
}
82+
}
83+
}
84+
```
85+
86+
## MCP Protocol Flow
87+
88+
1. **Initialize**: Client sends initialization message
89+
2. **Initialized**: Server confirms initialization
90+
3. **Tool Discovery**: Client can query available tools via `tools/list`
91+
4. **Tool Execution**: Client calls tools via `tools/call`
92+
5. **Resource Access**: Server can provide resources via `resources/list` and `resources/read`
93+
94+
## Implementation Details
95+
96+
### Console Connection Integration
97+
- Uses SuperSocket's `UseConsole()` extension
98+
- Leverages the new `ConsoleConnection` for stdio communication
99+
- Maintains full SuperSocket feature compatibility
100+
101+
### Pipeline Filter Optimization
102+
- Simplified from Content-Length headers to line-based JSON
103+
- Better suited for stdio communication patterns
104+
- Handles empty lines and malformed input gracefully
105+
106+
### Command-based Architecture
107+
- Uses SuperSocket's command system instead of legacy message handling
108+
- Proper separation of concerns
109+
- Extensible for additional MCP methods
110+
111+
## Logging Considerations
112+
113+
The server minimizes logging to stdout to avoid interfering with the MCP protocol. Important considerations:
114+
115+
- Logs warning level and above to stderr
116+
- Normal MCP communication happens on stdout
117+
- Debug information should be logged to files if needed
118+
119+
## Error Handling
120+
121+
The server includes comprehensive error handling for:
122+
- Malformed JSON messages
123+
- Invalid tool parameters
124+
- File system access errors
125+
- Network/stdio communication issues
126+
127+
All errors are returned as proper JSON-RPC error responses according to the MCP specification.

0 commit comments

Comments
 (0)