Improve redMCP server operations

This commit is contained in:
Jason Thistlethwaite
2026-04-25 02:46:58 +00:00
parent 05c1a4bc97
commit d54319a5bb
14 changed files with 420 additions and 24 deletions
+72 -5
View File
@@ -10,10 +10,12 @@ use Throwable;
final class McpDispatcher
{
private RedmineClient $redmine;
private McpDebugLogger $logger;
public function __construct(RedmineClient $redmine)
public function __construct(RedmineClient $redmine, ?McpDebugLogger $logger = null)
{
$this->redmine = $redmine;
$this->logger = $logger ?? new McpDebugLogger(null);
}
/**
@@ -21,18 +23,23 @@ final class McpDispatcher
*
* @return array<string,mixed>|null
*/
public function handleMessage(array $message): ?array
public function handleMessage(array $message, array $context = []): ?array
{
$id = $message['id'] ?? null;
if ($id === null) {
return null;
}
$started = microtime(true);
$method = (string) ($message['method'] ?? '');
$params = is_array($message['params'] ?? null) ? $message['params'] : [];
try {
$method = (string) ($message['method'] ?? '');
$params = is_array($message['params'] ?? null) ? $message['params'] : [];
return ['jsonrpc' => '2.0', 'id' => $id, 'result' => $this->dispatch($method, $params)];
$result = $this->dispatch($method, $params);
$this->logCall($context, $method, $params, true, $started);
return ['jsonrpc' => '2.0', 'id' => $id, 'result' => $result];
} catch (Throwable $exception) {
$this->logCall($context, $method, $params, false, $started, $exception->getMessage());
return [
'jsonrpc' => '2.0',
'id' => $id,
@@ -84,6 +91,13 @@ final class McpDispatcher
private function tools(): array
{
return [
$this->tool('redmine_list_projects', 'List Redmine projects using native /projects.json.', [
'params' => ['type' => 'object', 'description' => 'Redmine project list params such as include, offset, and limit.'],
]),
$this->tool('redmine_get_project', 'Fetch one Redmine project by id or identifier.', [
'project_id' => ['type' => ['string', 'integer'], 'description' => 'Redmine numeric project id or identifier.'],
'params' => ['type' => 'object', 'description' => 'Optional Redmine project params such as include=trackers,issue_categories,enabled_modules.'],
], ['project_id']),
$this->tool('redmine_list_issues', 'List Redmine issues using native /issues.json filters.', [
'filters' => ['type' => 'object', 'description' => 'Redmine issue list filters such as project_id, status_id, query_id, sort, offset, and limit.'],
]),
@@ -154,6 +168,12 @@ final class McpDispatcher
$arguments = is_array($params['arguments'] ?? null) ? $params['arguments'] : [];
switch ($name) {
case 'redmine_list_projects':
$result = $this->redmine->listProjects($this->objectArg($arguments, 'params'));
break;
case 'redmine_get_project':
$result = $this->redmine->project($this->projectIdArg($arguments, 'project_id'), $this->objectArg($arguments, 'params'));
break;
case 'redmine_list_issues':
$result = $this->redmine->filterIssues($this->objectArg($arguments, 'filters'));
break;
@@ -242,6 +262,21 @@ final class McpDispatcher
return (int) $arguments[$key];
}
/**
* @param array<string,mixed> $arguments
*/
private function projectIdArg(array $arguments, string $key): int|string
{
if (!isset($arguments[$key])) {
throw new RuntimeException($key . ' is required.');
}
if (is_int($arguments[$key])) {
return $arguments[$key];
}
return $this->stringArg($arguments, $key);
}
/**
* @param array<string,mixed> $arguments
* @param array<int,string> $default
@@ -256,4 +291,36 @@ final class McpDispatcher
return array_values(array_filter(array_map('strval', $arguments[$key])));
}
/**
* @param array<string,mixed> $context
* @param array<string,mixed> $params
*/
private function logCall(
array $context,
string $method,
array $params,
bool $ok,
float $started,
?string $error = null
): void {
$record = [
'transport' => $context['transport'] ?? 'unknown',
'client_ip' => $context['client_ip'] ?? null,
'method' => $method,
'params' => $params,
'ok' => $ok,
'duration_ms' => (int) round((microtime(true) - $started) * 1000),
];
if (isset($params['name'])) {
$record['tool_name'] = $params['name'];
$arguments = $params['arguments'] ?? null;
$record['arguments'] = is_array($arguments) ? $arguments : null;
}
if ($error !== null) {
$record['error'] = $error;
}
$this->logger->log($record);
}
}