Browse Source

refacto the mcp server service code to use the mcp package

pull/24677/head
Mansur Besleney 1 month ago
parent
commit
6007b2f587
  1. 27
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs
  2. 6
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs
  3. 8
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs
  4. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs
  5. 488
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs

27
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs

@ -58,14 +58,17 @@ public class CliService : ITransientDependency
var commandLineArgs = CommandLineArgumentParser.Parse(args);
var currentCliVersion = await CliVersionService.GetCurrentCliVersionAsync();
var isMcpCommand = commandLineArgs.IsCommand("mcp");
// Don't print banner for MCP command to avoid corrupting stdout JSON-RPC stream
if (!commandLineArgs.IsCommand("mcp"))
if (!isMcpCommand)
{
Logger.LogInformation($"ABP CLI {currentCliVersion}");
}
#if !DEBUG
if (!commandLineArgs.Options.ContainsKey("skip-cli-version-check"))
// Skip version check for MCP command to avoid corrupting stdout JSON-RPC stream
if (!isMcpCommand && !commandLineArgs.Options.ContainsKey("skip-cli-version-check"))
{
await CheckCliVersionAsync(currentCliVersion);
}
@ -89,13 +92,29 @@ public class CliService : ITransientDependency
}
catch (CliUsageException usageException)
{
Logger.LogWarning(usageException.Message);
// For MCP command, write errors to stderr to avoid corrupting stdout JSON-RPC stream
if (commandLineArgs.IsCommand("mcp"))
{
await Console.Error.WriteLineAsync($"[MCP] Error: {usageException.Message}");
}
else
{
Logger.LogWarning(usageException.Message);
}
Environment.ExitCode = 1;
}
catch (Exception ex)
{
await _telemetryService.AddErrorActivityAsync(ex.Message);
Logger.LogException(ex);
// For MCP command, write errors to stderr to avoid corrupting stdout JSON-RPC stream
if (commandLineArgs.IsCommand("mcp"))
{
await Console.Error.WriteLineAsync($"[MCP] Fatal error: {ex.Message}");
}
else
{
Logger.LogException(ex);
}
throw;
}
finally

6
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs

@ -17,6 +17,12 @@ public class CommandSelector : ICommandSelector, ITransientDependency
public Type Select(CommandLineArgs commandLineArgs)
{
// Don't fall back to HelpCommand for MCP command to avoid corrupting stdout JSON-RPC stream
if (commandLineArgs.IsCommand("mcp"))
{
return Options.Commands.GetOrDefault("mcp") ?? typeof(HelpCommand);
}
if (commandLineArgs.Command.IsNullOrWhiteSpace())
{
return typeof(HelpCommand);

8
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs

@ -32,6 +32,14 @@ public class HelpCommand : IConsoleCommand, ITransientDependency
public Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
// Don't output help text for MCP command to avoid corrupting stdout JSON-RPC stream
// If MCP command is being used, it should have been handled directly, not through HelpCommand
if (commandLineArgs.IsCommand("mcp"))
{
// Silently return - MCP server should handle its own errors
return Task.CompletedTask;
}
if (string.IsNullOrWhiteSpace(commandLineArgs.Target))
{
Logger.LogInformation(GetUsageInfo());

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs

@ -99,7 +99,7 @@ public class McpHttpClientService : ITransientDependency
var response = await httpClient.GetAsync(baseUrl);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
catch (Exception)
{
// Silently fail health check - it's optional
return false;

488
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs

@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Commands.Services;
@ -14,352 +14,256 @@ public class McpServerService : ITransientDependency
{
private readonly McpHttpClientService _mcpHttpClient;
private readonly ILogger<McpServerService> _logger;
private readonly ILoggerFactory _loggerFactory;
public McpServerService(
McpHttpClientService mcpHttpClient,
ILogger<McpServerService> logger)
ILogger<McpServerService> logger,
ILoggerFactory loggerFactory)
{
_mcpHttpClient = mcpHttpClient;
_logger = logger;
_loggerFactory = loggerFactory;
}
public async Task RunAsync(CancellationToken cancellationToken = default)
{
try
{
// Write to stderr to avoid corrupting stdout JSON-RPC stream
await Console.Error.WriteLineAsync("[MCP] ABP MCP Server started successfully");
// Log to stderr to avoid corrupting stdout JSON-RPC stream
await Console.Error.WriteLineAsync("[MCP] Starting ABP MCP Server (stdio)");
await ProcessStdioAsync(cancellationToken);
}
catch (OperationCanceledException)
{
await Console.Error.WriteLineAsync("[MCP] ABP MCP Server stopped");
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"[MCP] Error running ABP MCP Server: {ex.Message}");
throw;
}
var options = new McpServerOptions();
RegisterAllTools(options);
var server = McpServer.Create(
new StdioServerTransport("abp-mcp-server", _loggerFactory),
options
);
await server.RunAsync(cancellationToken);
await Console.Error.WriteLineAsync("[MCP] ABP MCP Server stopped");
}
private async Task ProcessStdioAsync(CancellationToken cancellationToken)
private void RegisterAllTools(McpServerOptions options)
{
using var reader = new StreamReader(Console.OpenStandardInput());
using var writer = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
while (!cancellationToken.IsCancellationRequested)
{
string line;
try
RegisterTool(
options,
"get_relevant_abp_documentation",
"Search ABP framework technical documentation including official guides, API references, and framework documentation.",
new
{
var readTask = reader.ReadLineAsync();
var completedTask = await Task.WhenAny(readTask, Task.Delay(Timeout.Infinite, cancellationToken));
if (completedTask != readTask)
type = "object",
properties = new
{
// Cancellation requested
break;
}
line = await readTask;
}
catch (OperationCanceledException)
{
break;
}
if (line == null)
{
// EOF reached
break;
}
// Skip empty lines or lines that are not JSON
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
// Check if line looks like JSON (starts with '{')
if (!line.TrimStart().StartsWith("{"))
{
// Not JSON, probably build output or other noise - log to stderr and skip
await Console.Error.WriteLineAsync($"[MCP] Skipping non-JSON line: {line.Substring(0, Math.Min(50, line.Length))}...");
continue;
query = new
{
type = "string",
description = "The search query to find relevant documentation"
}
},
required = new[] { "query" }
}
);
try
RegisterTool(
options,
"get_relevant_abp_articles",
"Search ABP blog posts, tutorials, and community-contributed content.",
new
{
var request = JsonSerializer.Deserialize<McpRequest>(line, new JsonSerializerOptions
type = "object",
properties = new
{
PropertyNameCaseInsensitive = true
});
var response = await HandleRequestAsync(request, cancellationToken);
var responseJson = JsonSerializer.Serialize(response, new JsonSerializerOptions
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
await writer.WriteLineAsync(responseJson);
}
catch (JsonException jsonEx)
{
// JSON parse error - log to stderr but don't send error response
// (the line might be build output or other noise)
await Console.Error.WriteLineAsync($"[MCP] JSON parse error: {jsonEx.Message} | Line: {line.Substring(0, Math.Min(100, line.Length))}");
query = new
{
type = "string",
description = "The search query to find relevant articles"
}
},
required = new[] { "query" }
}
catch (Exception ex)
);
RegisterTool(
options,
"get_relevant_abp_support_questions",
"Search support ticket history containing real-world problems and their solutions.",
new
{
// Other errors during request handling - send error response to client
await Console.Error.WriteLineAsync($"[MCP] Error processing request: {ex.Message}");
var errorResponse = new McpResponse
type = "object",
properties = new
{
Jsonrpc = "2.0",
Id = null,
Error = new McpError
query = new
{
Code = -32603,
Message = ex.Message
type = "string",
description = "The search query to find relevant support questions"
}
};
var errorJson = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
await writer.WriteLineAsync(errorJson);
},
required = new[] { "query" }
}
}
await Console.Error.WriteLineAsync("[MCP] Stdio processing loop ended");
}
);
private async Task<McpResponse> HandleRequestAsync(McpRequest request, CancellationToken cancellationToken)
{
if (request.Method == "initialize")
{
return new McpResponse
RegisterTool(
options,
"search_code",
"Search for code across ABP repositories using regex patterns.",
new
{
Jsonrpc = "2.0",
Id = request.Id,
Result = new
type = "object",
properties = new
{
protocolVersion = "2024-11-05",
capabilities = new
query = new
{
tools = new { }
type = "string",
description = "The regex pattern or search query to find code"
},
serverInfo = new
repo_filter = new
{
name = "abp-mcp-server",
version = "1.0.0"
type = "string",
description = "Optional repository filter to limit search scope"
}
}
};
}
if (request.Method == "tools/list")
{
return new McpResponse
{
Jsonrpc = "2.0",
Id = request.Id,
Result = new
{
tools = GetToolDefinitions()
}
};
}
if (request.Method == "tools/call")
{
var toolName = request.Params.GetProperty("name").GetString();
var arguments = request.Params.GetProperty("arguments");
var result = await _mcpHttpClient.CallToolAsync(toolName, arguments);
var toolResponse = JsonSerializer.Deserialize<JsonElement>(result);
return new McpResponse
{
Jsonrpc = "2.0",
Id = request.Id,
Result = toolResponse
};
}
},
required = new[] { "query" }
}
);
return new McpResponse
{
Jsonrpc = "2.0",
Id = request.Id,
Error = new McpError
RegisterTool(
options,
"list_repos",
"List all available ABP repositories in SourceBot.",
new
{
Code = -32601,
Message = $"Method not found: {request.Method}"
type = "object",
properties = new { }
}
};
}
);
private object[] GetToolDefinitions()
{
return new object[]
{
RegisterTool(
options,
"get_file_source",
"Retrieve the complete source code of a specific file from an ABP repository.",
new
{
name = "get_relevant_abp_documentation",
description = "Search ABP framework technical documentation including official guides, API references, and framework documentation.",
inputSchema = new
type = "object",
properties = new
{
type = "object",
properties = new
repoId = new
{
query = new
{
type = "string",
description = "The search query to find relevant documentation"
}
type = "string",
description = "The repository identifier containing the file"
},
required = new[] { "query" }
}
},
new
{
name = "get_relevant_abp_articles",
description = "Search ABP blog posts, tutorials, and community-contributed content.",
inputSchema = new
{
type = "object",
properties = new
fileName = new
{
query = new
{
type = "string",
description = "The search query to find relevant articles"
}
},
required = new[] { "query" }
}
},
new
type = "string",
description = "The file path or name to retrieve"
}
},
required = new[] { "repoId", "fileName" }
}
);
}
private void RegisterTool(
McpServerOptions options,
string name,
string description,
object inputSchema)
{
if (options.ToolCollection == null)
{
options.ToolCollection = new McpServerPrimitiveCollection<McpServerTool>();
}
var tool = new AbpMcpServerTool(
name,
description,
JsonSerializer.SerializeToElement(inputSchema),
async (context, cancellationToken) =>
{
name = "get_relevant_abp_support_questions",
description = "Search support ticket history containing real-world problems and their solutions.",
inputSchema = new
// Log to stderr to avoid corrupting stdout JSON-RPC stream
await Console.Error.WriteLineAsync($"[MCP] Tool '{name}' called with arguments: {context.Params.Arguments}");
try
{
type = "object",
properties = new
var argumentsDict = context.Params.Arguments;
var argumentsJson = JsonSerializer.SerializeToElement(argumentsDict);
var resultJson = await _mcpHttpClient.CallToolAsync(
name,
argumentsJson
);
await Console.Error.WriteLineAsync($"[MCP] Tool '{name}' executed successfully");
// Try to deserialize the response as CallToolResult
// The HTTP client should return JSON in the format expected by MCP
try
{
query = new
var callToolResult = JsonSerializer.Deserialize<CallToolResult>(resultJson);
if (callToolResult != null)
{
type = "string",
description = "The search query to find relevant support questions"
return callToolResult;
}
},
required = new[] { "query" }
}
},
new
{
name = "search_code",
description = "Search for code across ABP repositories using regex patterns.",
inputSchema = new
{
type = "object",
properties = new
}
catch (Exception deserializeEx)
{
query = new
{
type = "string",
description = "The regex pattern or search query to find code"
},
repo_filter = new
{
type = "string",
description = "Optional repository filter to limit search scope"
}
},
required = new[] { "query" }
}
},
new
{
name = "list_repos",
description = "List all available ABP repositories in SourceBot.",
inputSchema = new
{
type = "object",
properties = new { }
await Console.Error.WriteLineAsync($"[MCP] Failed to deserialize response as CallToolResult: {deserializeEx.Message}");
await Console.Error.WriteLineAsync($"[MCP] Response was: {resultJson.Substring(0, Math.Min(500, resultJson.Length))}");
}
// Fallback: return empty result if deserialization fails
return new CallToolResult
{
Content = new List<ContentBlock>()
};
}
},
new
{
name = "get_file_source",
description = "Retrieve the complete source code of a specific file from an ABP repository.",
inputSchema = new
catch (Exception ex)
{
type = "object",
properties = new
await Console.Error.WriteLineAsync($"[MCP] Tool '{name}' execution failed: {ex.Message}");
return new CallToolResult
{
repoId = new
{
type = "string",
description = "The repository identifier containing the file"
},
fileName = new
{
type = "string",
description = "The file path or name to retrieve"
}
},
required = new[] { "repoId", "fileName" }
Content = new List<ContentBlock>(),
IsError = true
};
}
}
};
);
options.ToolCollection.Add(tool);
}
}
internal class McpRequest
{
[System.Text.Json.Serialization.JsonPropertyName("jsonrpc")]
public string Jsonrpc { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("id")]
public object Id { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("method")]
public string Method { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("params")]
public JsonElement Params { get; set; }
}
private class AbpMcpServerTool : McpServerTool
{
private readonly string _name;
private readonly string _description;
private readonly JsonElement _inputSchema;
private readonly Func<RequestContext<CallToolRequestParams>, CancellationToken, ValueTask<CallToolResult>> _handler;
internal class McpResponse
{
[System.Text.Json.Serialization.JsonPropertyName("jsonrpc")]
public string Jsonrpc { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("id")]
public object Id { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("result")]
public object Result { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("error")]
public McpError Error { get; set; }
}
public AbpMcpServerTool(
string name,
string description,
JsonElement inputSchema,
Func<RequestContext<CallToolRequestParams>, CancellationToken, ValueTask<CallToolResult>> handler)
{
_name = name;
_description = description;
_inputSchema = inputSchema;
_handler = handler;
}
internal class McpError
{
[System.Text.Json.Serialization.JsonPropertyName("code")]
public int Code { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("message")]
public string Message { get; set; }
}
public override Tool ProtocolTool => new Tool
{
Name = _name,
Description = _description,
InputSchema = _inputSchema
};
public override IReadOnlyList<object> Metadata => Array.Empty<object>();
public override ValueTask<CallToolResult> InvokeAsync(RequestContext<CallToolRequestParams> context, CancellationToken cancellationToken)
{
return _handler(context, cancellationToken);
}
}
}

Loading…
Cancel
Save