Skip to content

Commit 4e728da

Browse files
committed
add mcp tool
1 parent 34464f1 commit 4e728da

File tree

8 files changed

+180
-6
lines changed

8 files changed

+180
-6
lines changed

config/cortex.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,22 @@
211211
'local_http' => [
212212
'transport' => 'http',
213213
'url' => 'http://localhost:3001/sse',
214-
'headers' => null,
215214
],
215+
216+
// 'tavily' => [
217+
// 'transport' => 'http',
218+
// 'url' => 'https://mcp.tavily.com/mcp/?tavilyApiKey=' . env('TAVILY_API_KEY'),
219+
// ],
220+
221+
// 'tavily' => [
222+
// 'transport' => 'stdio',
223+
// 'command' => 'npx',
224+
// 'args' => [
225+
// '-y',
226+
// 'mcp-remote',
227+
// 'https://mcp.tavily.com/mcp/?tavilyApiKey=' . env('TAVILY_API_KEY'),
228+
// ],
229+
// ],
216230
],
217231

218232
/*

src/LLM/Contracts/Tool.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function format(): array;
3737
*
3838
* @param ToolCall|array<int, mixed> $arguments
3939
*/
40-
public function invoke(ToolCall|array $arguments): mixed;
40+
public function invoke(ToolCall|array $arguments = []): mixed;
4141

4242
/**
4343
* Invoke the tool as a tool message.

src/Tools/ClosureTool.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function schema(): ObjectSchema
4545
/**
4646
* @param ToolCall|array<string, mixed> $toolCall
4747
*/
48-
public function invoke(ToolCall|array $toolCall): mixed
48+
public function invoke(ToolCall|array $toolCall = []): mixed
4949
{
5050
// Get the arguments from the given tool call.
5151
$arguments = $this->getArguments($toolCall);

src/Tools/GoogleSerper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function schema(): ObjectSchema
3838
/**
3939
* @param ToolCall|array<int, mixed> $toolCall
4040
*/
41-
public function invoke(ToolCall|array $toolCall): string
41+
public function invoke(ToolCall|array $toolCall = []): string
4242
{
4343
$arguments = $this->getArguments($toolCall);
4444
$searchQuery = $arguments['query'];

src/Tools/McpTool.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cortex\Tools;
6+
7+
use PhpMcp\Client\Client;
8+
use Cortex\Facades\McpServer;
9+
use Cortex\LLM\Data\ToolCall;
10+
use Cortex\JsonSchema\SchemaFactory;
11+
use Cortex\Exceptions\GenericException;
12+
use Cortex\JsonSchema\Types\ObjectSchema;
13+
use PhpMcp\Client\Model\Content\TextContent;
14+
use PhpMcp\Client\Contracts\ContentInterface;
15+
use PhpMcp\Client\Model\Definitions\ToolDefinition;
16+
17+
class McpTool extends AbstractTool
18+
{
19+
protected Client $client;
20+
21+
protected ObjectSchema $schema;
22+
23+
protected string $description;
24+
25+
public function __construct(
26+
protected string $name,
27+
protected ?string $mcpServer = null,
28+
) {
29+
$this->client = McpServer::driver($this->mcpServer);
30+
$toolDefinition = $this->getToolDefinition();
31+
$this->description = $toolDefinition->description ?? '';
32+
$schema = SchemaFactory::fromJson($toolDefinition->inputSchema);
33+
34+
if (! $schema instanceof ObjectSchema) {
35+
throw new GenericException(sprintf('Schema for tool %s is not an object', $this->name));
36+
}
37+
38+
if ($toolDefinition->description !== null) {
39+
$schema->description($toolDefinition->description);
40+
}
41+
42+
$this->schema = $schema;
43+
}
44+
45+
public function name(): string
46+
{
47+
return $this->name;
48+
}
49+
50+
public function description(): string
51+
{
52+
return $this->description;
53+
}
54+
55+
public function schema(): ObjectSchema
56+
{
57+
return $this->schema;
58+
}
59+
60+
/**
61+
* @param ToolCall|array<string, mixed> $toolCall
62+
*/
63+
public function invoke(ToolCall|array $toolCall = []): mixed
64+
{
65+
try {
66+
$this->client->initialize();
67+
68+
// Get the arguments from the given tool call.
69+
$arguments = $this->getArguments($toolCall);
70+
71+
// Ensure arguments are valid as per the tool's schema.
72+
if ($this->schema->getPropertyKeys() !== []) {
73+
$this->schema->validate($arguments);
74+
}
75+
76+
$result = $this->client->callTool($this->name, $arguments);
77+
} finally {
78+
$this->client->disconnect();
79+
}
80+
81+
// Only support text content for now.
82+
$textContent = collect($result->content)
83+
->filter(fn(ContentInterface $item): bool => $item instanceof TextContent)
84+
->first();
85+
86+
return $textContent?->text;
87+
}
88+
89+
protected function getToolDefinition(): ToolDefinition
90+
{
91+
$this->client->initialize();
92+
$tools = $this->client->listTools();
93+
94+
$tool = collect($tools)->firstWhere('name', $this->name);
95+
96+
if ($tool === null) {
97+
throw new GenericException(sprintf('Tool %s not found in MCP server %s', $this->name, $this->mcpServer));
98+
}
99+
100+
return $tool;
101+
}
102+
103+
public function __destruct()
104+
{
105+
$this->client->disconnect();
106+
}
107+
}

src/Tools/SchemaTool.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function schema(): ObjectSchema
3838
/**
3939
* @param ToolCall|array<int, mixed> $toolCall
4040
*/
41-
public function invoke(ToolCall|array $toolCall): mixed
41+
public function invoke(ToolCall|array $toolCall = []): mixed
4242
{
4343
throw new GenericException(
4444
'The Schema tool does not support invocation. It is only used for structured output.',

src/Tools/TavilySearch.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function schema(): ObjectSchema
3838
/**
3939
* @param ToolCall|array<int, mixed> $toolCall
4040
*/
41-
public function invoke(ToolCall|array $toolCall): string
41+
public function invoke(ToolCall|array $toolCall = []): string
4242
{
4343
$arguments = $this->getArguments($toolCall);
4444
$searchQuery = $arguments['query'];

tests/Unit/Tools/McpToolTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cortex\Tests\Unit\Tools;
6+
7+
use Cortex\Tools\McpTool;
8+
9+
test('it can create a schema from an MCP tool', function (): void {
10+
// $tool = new McpTool(
11+
// name: 'tavily_search',
12+
// mcpServer: 'tavily',
13+
// );
14+
15+
$tool = new McpTool('echo');
16+
17+
// dump($tool->schema()->toArray());
18+
// dump($tool->description());
19+
$result = $tool->invoke([
20+
'message' => 'foo',
21+
]);
22+
23+
dd($result);
24+
25+
// dd(json_decode($result, true));
26+
27+
// expect($tool->schema())->toBeInstanceOf(Schema::class);
28+
// expect($tool->name())->toBe('multiply');
29+
// expect($tool->description())->toBe('Multiply two numbers');
30+
// expect($tool->invoke([
31+
// 'a' => 2,
32+
// 'b' => 2,
33+
// ]))->toBe(4);
34+
35+
// expect($tool->format())->toBe([
36+
// 'name' => 'multiply',
37+
// 'description' => 'Multiply two numbers',
38+
// 'parameters' => [
39+
// 'type' => 'object',
40+
// 'properties' => [
41+
// 'a' => [
42+
// 'type' => 'integer',
43+
// 'description' => 'The first number',
44+
// ],
45+
// 'b' => [
46+
// 'type' => 'integer',
47+
// 'description' => 'The second number',
48+
// ],
49+
// ],
50+
// 'required' => ['a', 'b'],
51+
// ],
52+
// ]);
53+
})->todo();

0 commit comments

Comments
 (0)