diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..cc74aa5c0 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Klavis Strata MCP API Key +# Get your API key from: https://strata.klavis.ai/ +KLAVIS_STRATA_API_KEY=your-api-key-here diff --git a/config/api.php b/config/api.php index d6e883600..217ed9e81 100644 --- a/config/api.php +++ b/config/api.php @@ -186,6 +186,33 @@ ], + /* + |-------------------------------------------------------------------------- + | MCP Transports + |-------------------------------------------------------------------------- + | + | Model Context Protocol (MCP) transports configuration. Define your + | MCP servers and their connection settings here. Supported transport + | types include 'http' for HTTP/HTTPS connections. + | + */ + + 'mcp' => [ + 'transports' => [ + 'klavis-strata' => [ + 'type' => 'http', + 'url' => 'https://strata.klavis.ai/mcp/?strata_id=3befb976-1fc9-4ff0-9e87-a173b12657c6', + 'options' => [ + 'timeout' => 30, + 'verify' => true, + 'headers' => [ + 'Authorization' => 'Bearer ' . env('KLAVIS_STRATA_API_KEY'), + ], + ], + ], + ], + ], + /* |-------------------------------------------------------------------------- | Response Transformer diff --git a/src/Contract/Mcp/Transport.php b/src/Contract/Mcp/Transport.php new file mode 100644 index 000000000..a8bb695dc --- /dev/null +++ b/src/Contract/Mcp/Transport.php @@ -0,0 +1,37 @@ +container = $container; + $this->transports = $transports; + } + + /** + * Get a transport by name. + * + * @param string $name + * + * @throws \InvalidArgumentException + * + * @return \Dingo\Api\Contract\Mcp\Transport + */ + public function transport($name) + { + if (!isset($this->transports[$name])) { + throw new \InvalidArgumentException("MCP transport [{$name}] is not registered."); + } + + return $this->transports[$name]; + } + + /** + * Get all registered transports. + * + * @return array + */ + public function getTransports() + { + return $this->transports; + } + + /** + * Register a new transport. + * + * @param string $name + * @param \Dingo\Api\Contract\Mcp\Transport|callable $transport + * Either a transport instance or a callable that returns a transport. + * If a callable is provided, it will be invoked with the container. + * + * @return void + */ + public function registerTransport($name, $transport) + { + if (is_callable($transport)) { + $transport = call_user_func($transport, $this->container); + } + $this->transports[$name] = $transport; + } +} diff --git a/src/Mcp/Transport/HttpTransport.php b/src/Mcp/Transport/HttpTransport.php new file mode 100644 index 000000000..1fd84651d --- /dev/null +++ b/src/Mcp/Transport/HttpTransport.php @@ -0,0 +1,179 @@ +name = $name; + $this->url = $url; + $this->headers = $options['headers'] ?? []; + + $this->client = new Client([ + 'base_uri' => $url, + 'timeout' => $options['timeout'] ?? 30, + 'verify' => $options['verify'] ?? true, + ]); + } + + /** + * Connect to the MCP server and establish a session. + * + * @return mixed + */ + public function connect() + { + try { + // Test connection with a ping or initialization request + $response = $this->client->get('', [ + 'headers' => array_merge([ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], $this->headers), + ]); + + $this->connected = $response->getStatusCode() === 200; + + return $this->connected; + } catch (GuzzleException $e) { + $this->connected = false; + throw new \RuntimeException( + "Failed to connect to MCP server [{$this->name}] at {$this->url}: {$e->getMessage()}" + ); + } + } + + /** + * Send a request to the MCP server. + * + * @param string $method + * @param array $params + * + * @return mixed + */ + public function send($method, array $params = []) + { + if (!$this->connected) { + $this->connect(); + } + + try { + $response = $this->client->post('', [ + 'headers' => array_merge([ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], $this->headers), + 'json' => [ + 'jsonrpc' => '2.0', + 'method' => $method, + 'params' => $params, + 'id' => uniqid('mcp_', true), + ], + ]); + + $body = json_decode($response->getBody()->getContents(), true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException('Invalid JSON response from MCP server'); + } + + if (isset($body['error'])) { + throw new \RuntimeException( + "MCP server error: {$body['error']['message']} (code: {$body['error']['code']})" + ); + } + + return $body['result'] ?? null; + } catch (GuzzleException $e) { + throw new \RuntimeException( + "Failed to send request to MCP server [{$this->name}]: {$e->getMessage()}" + ); + } + } + + /** + * Close the connection to the MCP server. + * + * @return void + */ + public function disconnect() + { + $this->connected = false; + } + + /** + * Check if the transport is connected. + * + * @return bool + */ + public function isConnected() + { + return $this->connected; + } + + /** + * Get the transport name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the transport URL. + * + * @return string + */ + public function getUrl() + { + return $this->url; + } +} diff --git a/src/Provider/DingoServiceProvider.php b/src/Provider/DingoServiceProvider.php index a86fdd7e5..626aa70a4 100644 --- a/src/Provider/DingoServiceProvider.php +++ b/src/Provider/DingoServiceProvider.php @@ -52,6 +52,8 @@ public function register() $this->app->register(HttpServiceProvider::class); + $this->app->register(McpServiceProvider::class); + $this->registerExceptionHandler(); $this->registerDispatcher(); @@ -102,6 +104,7 @@ protected function registerClassAliases() 'api.limiting' => \Dingo\Api\Http\RateLimit\Handler::class, 'api.transformer' => \Dingo\Api\Transformer\Factory::class, 'api.url' => \Dingo\Api\Routing\UrlGenerator::class, + 'api.mcp' => \Dingo\Api\Mcp\Mcp::class, 'api.exception' => [\Dingo\Api\Exception\Handler::class, \Dingo\Api\Contract\Debug\ExceptionHandler::class], ]; diff --git a/src/Provider/McpServiceProvider.php b/src/Provider/McpServiceProvider.php new file mode 100644 index 000000000..8df487c9b --- /dev/null +++ b/src/Provider/McpServiceProvider.php @@ -0,0 +1,52 @@ +registerMcp(); + } + + /** + * Register the MCP instance. + * + * @return void + */ + protected function registerMcp() + { + $this->app->singleton('api.mcp', function ($app) { + $transports = []; + + // Get MCP configuration + $config = $this->config('mcp', []); + + // Register configured transports + if (isset($config['transports']) && is_array($config['transports'])) { + foreach ($config['transports'] as $name => $transportConfig) { + if (isset($transportConfig['type']) && $transportConfig['type'] === 'http') { + $transports[$name] = new HttpTransport( + $name, + $transportConfig['url'], + $transportConfig['options'] ?? [] + ); + } + } + } + + return new Mcp($app, $transports); + }); + + // Register alias + $this->app->alias('api.mcp', Mcp::class); + } +}