Add redMCP user and membership tools
This commit is contained in:
@@ -98,6 +98,17 @@ final class McpDispatcher
|
||||
'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_project_memberships', 'List users/groups and roles for a Redmine project.', [
|
||||
'project_id' => ['type' => ['string', 'integer'], 'description' => 'Redmine numeric project id or identifier.'],
|
||||
'params' => ['type' => 'object', 'description' => 'Redmine membership list params such as offset and limit.'],
|
||||
], ['project_id']),
|
||||
$this->tool('redmine_list_users', 'List Redmine users using native /users.json.', [
|
||||
'params' => ['type' => 'object', 'description' => 'Redmine user list params such as status, name, group_id, offset, and limit.'],
|
||||
]),
|
||||
$this->tool('redmine_get_user', 'Fetch one Redmine user by id.', [
|
||||
'user_id' => ['type' => 'integer'],
|
||||
'params' => ['type' => 'object', 'description' => 'Optional Redmine user params such as include=memberships,groups.'],
|
||||
], ['user_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.'],
|
||||
]),
|
||||
@@ -174,6 +185,15 @@ final class McpDispatcher
|
||||
case 'redmine_get_project':
|
||||
$result = $this->redmine->project($this->projectIdArg($arguments, 'project_id'), $this->objectArg($arguments, 'params'));
|
||||
break;
|
||||
case 'redmine_list_project_memberships':
|
||||
$result = $this->redmine->projectMemberships($this->projectIdArg($arguments, 'project_id'), $this->objectArg($arguments, 'params'));
|
||||
break;
|
||||
case 'redmine_list_users':
|
||||
$result = $this->redmine->listUsers($this->objectArg($arguments, 'params'));
|
||||
break;
|
||||
case 'redmine_get_user':
|
||||
$result = $this->redmine->user($this->intArg($arguments, 'user_id'), $this->objectArg($arguments, 'params'));
|
||||
break;
|
||||
case 'redmine_list_issues':
|
||||
$result = $this->redmine->filterIssues($this->objectArg($arguments, 'filters'));
|
||||
break;
|
||||
@@ -209,7 +229,7 @@ final class McpDispatcher
|
||||
throw new RuntimeException('Unknown tool: ' . $name);
|
||||
}
|
||||
|
||||
$encoded = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
$encoded = json_encode($this->redactSensitive($result), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
if ($encoded === false) {
|
||||
throw new RuntimeException('Could not encode tool result.');
|
||||
}
|
||||
@@ -308,14 +328,14 @@ final class McpDispatcher
|
||||
'transport' => $context['transport'] ?? 'unknown',
|
||||
'client_ip' => $context['client_ip'] ?? null,
|
||||
'method' => $method,
|
||||
'params' => $params,
|
||||
'params' => $this->redactSensitive($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;
|
||||
$record['arguments'] = is_array($arguments) ? $this->redactSensitive($arguments) : null;
|
||||
}
|
||||
if ($error !== null) {
|
||||
$record['error'] = $error;
|
||||
@@ -323,4 +343,42 @@ final class McpDispatcher
|
||||
|
||||
$this->logger->log($record);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function redactSensitive($value)
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$redacted = [];
|
||||
foreach ($value as $key => $item) {
|
||||
if (is_string($key) && $this->isSensitiveKey($key)) {
|
||||
$redacted[$key] = '[redacted]';
|
||||
continue;
|
||||
}
|
||||
|
||||
$redacted[$key] = $this->redactSensitive($item);
|
||||
}
|
||||
|
||||
return $redacted;
|
||||
}
|
||||
|
||||
private function isSensitiveKey(string $key): bool
|
||||
{
|
||||
$normalized = strtolower(str_replace(['-', '_'], '', $key));
|
||||
|
||||
return in_array($normalized, [
|
||||
'apikey',
|
||||
'authorization',
|
||||
'bearertoken',
|
||||
'password',
|
||||
'secret',
|
||||
'token',
|
||||
], true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,68 @@ final class RedmineClient
|
||||
return $response['project'] ?? $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* List Redmine users.
|
||||
*
|
||||
* @param array<string,mixed> $params Standard Redmine user list params.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function users(array $params = []): array
|
||||
{
|
||||
return $this->listUsers($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* List Redmine users.
|
||||
*
|
||||
* @param array<string,mixed> $params Standard Redmine user list params.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function listUsers(array $params = []): array
|
||||
{
|
||||
return $this->getJson('/users', $params) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one Redmine user.
|
||||
*
|
||||
* @param array<string,mixed> $params Standard Redmine user show params.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function user(int $userId, array $params = []): array
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
throw new RuntimeException('Fetching a user requires a positive user id.');
|
||||
}
|
||||
|
||||
$response = $this->getJson('/users/' . rawurlencode((string) $userId), $params);
|
||||
if (!is_array($response)) {
|
||||
throw new RuntimeException('Could not fetch user #' . $userId . '.');
|
||||
}
|
||||
|
||||
return $response['user'] ?? $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* List memberships for a Redmine project.
|
||||
*
|
||||
* @param array<string,mixed> $params Standard Redmine membership list params.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function projectMemberships(int|string $projectId, array $params = []): array
|
||||
{
|
||||
$projectId = trim((string) $projectId);
|
||||
if ($projectId === '') {
|
||||
throw new RuntimeException('Fetching project memberships requires a project id or identifier.');
|
||||
}
|
||||
|
||||
return $this->getJson('/projects/' . rawurlencode($projectId) . '/memberships', $params) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a normal Redmine issue.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user