Skip to content

Commit 797665f

Browse files
committed
feat: Add Context7 documentation search tool
1 parent 35d855a commit 797665f

File tree

4 files changed

+156
-2
lines changed

4 files changed

+156
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Action\Tools\Docs;
6+
7+
use Butschster\ContextGenerator\Lib\HttpClient\HttpClientInterface;
8+
use Butschster\ContextGenerator\McpServer\Attribute\InputSchema;
9+
use Butschster\ContextGenerator\McpServer\Attribute\Tool;
10+
use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post;
11+
use Mcp\Types\CallToolResult;
12+
use Mcp\Types\TextContent;
13+
use Psr\Http\Message\ServerRequestInterface;
14+
use Psr\Log\LoggerInterface;
15+
16+
#[Tool(
17+
name: 'docs-search',
18+
description: 'Search for documentation libraries available. Use "context-request" tool to get the context documents.',
19+
)]
20+
#[InputSchema(
21+
name: 'query',
22+
type: 'string',
23+
description: 'The search query to find documentation libraries',
24+
required: true,
25+
)]
26+
final readonly class DocsSearchAction
27+
{
28+
private const string CONTEXT7_SEARCH_URL = 'https://context7.com/api/v1/search';
29+
30+
public function __construct(
31+
private LoggerInterface $logger,
32+
private HttpClientInterface $httpClient,
33+
) {}
34+
35+
#[Post(path: '/tools/call/docs-search', name: 'tools.docs-search')]
36+
public function __invoke(ServerRequestInterface $request): CallToolResult
37+
{
38+
$this->logger->info('Processing docs-search tool');
39+
40+
// Get params from the parsed body for POST requests
41+
$parsedBody = $request->getParsedBody();
42+
$query = \trim($parsedBody['query'] ?? '');
43+
44+
if (empty($query)) {
45+
return new CallToolResult([
46+
new TextContent(
47+
text: 'Error: Missing query parameter',
48+
),
49+
], isError: true);
50+
}
51+
52+
try {
53+
$url = self::CONTEXT7_SEARCH_URL . '?' . \http_build_query(['query' => $query]);
54+
55+
$this->logger->debug('Sending request to Context7 search API', [
56+
'url' => $url,
57+
]);
58+
59+
$headers = [
60+
'User-Agent' => 'CTX Bot',
61+
'Accept' => 'application/json',
62+
];
63+
64+
$response = $this->httpClient->get($url, $headers);
65+
66+
if (!$response->isSuccess()) {
67+
$statusCode = $response->getStatusCode();
68+
$this->logger->error('Context7 search request failed', [
69+
'statusCode' => $statusCode,
70+
]);
71+
return new CallToolResult([
72+
new TextContent(
73+
text: "Context7 search request failed with status code {$statusCode}",
74+
),
75+
], isError: true);
76+
}
77+
78+
$data = $response->getJson(true);
79+
80+
if (!isset($data['results']) || !\is_array($data['results'])) {
81+
$this->logger->warning('Unexpected response format from Context7', [
82+
'response' => $data,
83+
]);
84+
return new CallToolResult([
85+
new TextContent(
86+
text: 'Unexpected response format from Context7 search API',
87+
),
88+
], isError: true);
89+
}
90+
91+
// Limit the number of results if needed
92+
$results = $data['results'];
93+
94+
$this->logger->info('Documentation libraries found', [
95+
'count' => \count($results),
96+
'query' => $query,
97+
]);
98+
99+
// Format the results for display
100+
$formattedResults = \array_map(static fn(array $library) => [
101+
'id' => $library['id'] ?? '',
102+
'title' => $library['title'] ?? '',
103+
'description' => $library['description'] ?? '',
104+
'branch' => $library['branch'] ?? 'main',
105+
'lastUpdateDate' => $library['lastUpdateDate'] ?? '',
106+
'totalTokens' => $library['totalTokens'] ?? 0,
107+
'totalPages' => $library['totalPages'] ?? 0,
108+
'usage' => \sprintf(
109+
"Use in your context config: { type: 'docs', library: '%s', topic: 'your-topic' }",
110+
\ltrim($library['id'] ?? '', '/'),
111+
),
112+
], $results);
113+
114+
return new CallToolResult([
115+
new TextContent(
116+
text: \json_encode([
117+
'count' => \count($formattedResults),
118+
'results' => $formattedResults,
119+
], JSON_PRETTY_PRINT),
120+
),
121+
]);
122+
} catch (\Throwable $e) {
123+
$this->logger->error('Error searching documentation libraries', [
124+
'query' => $query,
125+
'error' => $e->getMessage(),
126+
'trace' => $e->getTraceAsString(),
127+
]);
128+
129+
return new CallToolResult([
130+
new TextContent(
131+
text: 'Error: ' . $e->getMessage(),
132+
),
133+
], isError: true);
134+
}
135+
}
136+
}

src/McpServer/McpConfig.php

+8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ final class McpConfig extends InjectableConfig
2727
'prompt_operations' => [
2828
'enable' => true,
2929
],
30+
'docs_tools' => [
31+
'enable' => true,
32+
],
3033
'custom_tools' => [
3134
'enable' => true,
3235
'max_runtime' => 30,
@@ -72,6 +75,11 @@ public function isPromptOperationsEnabled(): bool
7275
return $this->config['prompt_operations']['enable'] ?? false;
7376
}
7477

78+
public function isDocsToolsEnabled(): bool
79+
{
80+
return $this->config['docs_tools']['enable'] ?? true;
81+
}
82+
7583
public function commonPromptsEnabled(): bool
7684
{
7785
return $this->config['common_prompts']['enable'] ?? true;

src/McpServer/McpServerBootloader.php

+11-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Butschster\ContextGenerator\McpServer\Action\Prompts\ProjectStructurePromptAction;
1313
use Butschster\ContextGenerator\McpServer\Action\Resources\GetDocumentContentResourceAction;
1414
use Butschster\ContextGenerator\McpServer\Action\Resources\JsonSchemaResourceAction;
15+
use Butschster\ContextGenerator\McpServer\Action\Tools\Docs\DocsSearchAction;
1516
use Butschster\ContextGenerator\McpServer\Action\Resources\ListResourcesAction;
1617
use Butschster\ContextGenerator\McpServer\Action\Tools\Context\ContextAction;
1718
use Butschster\ContextGenerator\McpServer\Action\Tools\Context\ContextGetAction;
@@ -80,6 +81,9 @@ public function init(EnvironmentInterface $env): void
8081
'context_operations' => [
8182
'enable' => (bool) $env->get('MCP_CONTEXT_OPERATIONS', !$isCommonProject),
8283
],
84+
'docs_tools' => [
85+
'enable' => (bool) $env->get('MCP_DOCS_TOOLS_ENABLED', true),
86+
],
8387
'prompt_operations' => [
8488
'enable' => (bool) $env->get('MCP_PROMPT_OPERATIONS', false),
8589
],
@@ -88,7 +92,7 @@ public function init(EnvironmentInterface $env): void
8892
'max_runtime' => (int) $env->get('MCP_TOOL_MAX_RUNTIME', 30),
8993
],
9094
'common_prompts' => [
91-
'enable' => (bool) $env->get('MCP_COMMON_PROMPTS', true),
95+
'enable' => (bool) $env->get('MCP_COMMON_PROMPTS', true),
9296
],
9397
],
9498
);
@@ -132,7 +136,6 @@ interface: ProjectServiceInterface::class,
132136

133137
private function actions(McpConfig $config): array
134138
{
135-
136139
$actions = [
137140
// Prompts controllers
138141
GetPromptAction::class,
@@ -172,6 +175,12 @@ private function actions(McpConfig $config): array
172175
];
173176
}
174177

178+
if ($config->isDocsToolsEnabled()) {
179+
$actions = [
180+
...$actions,
181+
DocsSearchAction::class,
182+
];
183+
}
175184
if ($config->isFileOperationsEnabled()) {
176185
$actions = [
177186
...$actions,

src/McpServer/context.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ documents:
3030
- type: file
3131
sourcePaths:
3232
- ./Tool
33+
- ./Action/Tools
3334

3435
- description: "MCP Server routing"
3536
outputPath: "mcp/routing.md"

0 commit comments

Comments
 (0)