diff --git a/code_samples/mcp/config/packages/mcp.yaml b/code_samples/mcp/config/packages/mcp.yaml new file mode 100644 index 0000000000..137ecb0e69 --- /dev/null +++ b/code_samples/mcp/config/packages/mcp.yaml @@ -0,0 +1,18 @@ +ibexa: + repositories: + default: + mcp: + example: + path: /mcp/example + enabled: true + description: 'Example MCP Server' + instructions: 'Use this server to greet someone.' + discovery_cache: cache.tagaware.filesystem + session: + type: file + directory: '%kernel.cache_dir%/mcp/sessions' + system: + default: + mcp: + servers: + - example diff --git a/code_samples/mcp/mcp.sh b/code_samples/mcp/mcp.sh new file mode 100644 index 0000000000..7a0d943fb4 --- /dev/null +++ b/code_samples/mcp/mcp.sh @@ -0,0 +1,60 @@ +baseUrl='http://localhost' # Adapt to your test case + +jwtToken=$(curl -s -X 'POST' \ + "$baseUrl/api/ibexa/v2/user/token/jwt" \ + -H 'Content-Type: application/json' \ + -d '{ + "JWTInput": { + "_media-type": "application/vnd.ibexa.api.JWTInput", + "username": "admin", + "password": "publish" + } + }' | jq -r .JWT.token) + +mcpSessionId=$(curl -s -i -X 'POST' "$baseUrl/mcp/example" \ + -H "Authorization: Bearer $jwtToken" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "test-curl-client", + "version": "1.0.0" + } + } + }' | grep 'Mcp-Session-Id:' | sed 's/Mcp-Session-Id: \([0-9a-f-]*\).*/\1/') + +curl -s -i -X 'POST' "$baseUrl/mcp/example" \ + -H "Authorization: Bearer $jwtToken" \ + -H "Mcp-Session-Id: $mcpSessionId" \ + -d '{ + "jsonrpc": "2.0", + "method": "notifications/initialized" + }' + +curl -s -X 'POST' "$baseUrl/mcp/example" \ + -H "Authorization: Bearer $jwtToken" \ + -H "Mcp-Session-Id: $mcpSessionId" \ + -d '{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list" + }' | jq + +curl -s -X 'POST' "$baseUrl/mcp/example" \ + -H "Authorization: Bearer $jwtToken" \ + -H "Mcp-Session-Id: $mcpSessionId" \ + -d '{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "greet", + "arguments": { + "name": "World" + } + } + }' | jq diff --git a/code_samples/mcp/src/Command/McpServerListCommand.php b/code_samples/mcp/src/Command/McpServerListCommand.php new file mode 100644 index 0000000000..c3b48dbce9 --- /dev/null +++ b/code_samples/mcp/src/Command/McpServerListCommand.php @@ -0,0 +1,26 @@ +configRegistry->getServerConfigurations() as $serverConfiguration) { + $io->title($serverConfiguration->identifier); + dump($serverConfiguration); + } + + return Command::SUCCESS; + } +} diff --git a/code_samples/mcp/src/Mcp/ExampleTools.php b/code_samples/mcp/src/Mcp/ExampleTools.php new file mode 100644 index 0000000000..ee19659065 --- /dev/null +++ b/code_samples/mcp/src/Mcp/ExampleTools.php @@ -0,0 +1,15 @@ +: + mcp: + : + path: + enabled: true + # Server options… + discovery_cache: + session: + type: + # Session options… + system: + : + mcp: + servers: + - +``` + +TODO: `ddev php bin/console debug:router --siteaccess=` should list some `ibexa.mcp. GET|POST|DELETE|OPTIONS ` + +TODO: Maybe explain that routes are built automatically from MCP server `path` configs thank to `config/routes/ibexa_mcp.yaml` and `\Ibexa\Bundle\Mcp\Routing\McpRouteLoader` + +### MCP server options + +| Option | Type | Required | Default | Description | +|-------------------|---------|----------|---------|-----------------------------------------------| +| `path` | string | Yes | | MCP server endpoint path | +| `enabled` | boolean | No | `false` | Whether the server is enabled | +| `version` | string | No | `1.0.0` | MCP server version | +| `description` | string | No | `null` | Human-readable server description | +| `instructions` | string | No | `null` | Instructions dedicated for LLM interaction | +| `discovery_cache` | string | Yes | | PSR-6 ou PSR-16 cache pool service identifier | +| `session` | object | Yes | | Session storage configuration | + +Notice that a server is disabled by default, it needs to be explicitly enabled. + +### MCP server discovery cache + +TODO + +### MCP server session storage + +#### Options + +| Option | Type | Default | Description | +|-------------|---------|----------|---------------------------------------------------| +| `type` | enum | `memory` | Session store type: `psr16`, `file`, or `memory` | +| `service` | string | `null` | PSR-16 cache service ID for `psr16` session store | +| `prefix` | string | `mcp_` | Key prefix for `psr16` session store | +| `directory` | string | `null` | Directory path for `file` session store | +| `ttl` | integer | `3600` | Session TTL in seconds | + +#### PSR-16 + +Sessions are stored using a PSR-16 compatible cache implementation. Requires service option pointing to a valid cache service ID. + +```yaml + session: + type: psr16 + service: cache.redis.mcp + prefix: 'mcp__' +services: + cache.redis.mcp: + public: true + class: Symfony\Component\Cache\Adapter\RedisTagAwareAdapter + parent: cache.adapter.redis + tags: + - name: cache.pool + clearer: cache.app_clearer + provider: 'redis://mcp.redis:6379' + namespace: 'mcp' +``` + +#### File + +Sessions are persisted to the filesystem. Requires directory option to be set. + +```yaml + session: + type: file + directory: '%kernel.cache_dir%/mcp/sessions' +``` + +#### Memory + +Sessions are stored in memory. Suitable for development and STDIO transport. + +TODO: Might not work with DDEV or Docker + +```yaml + session: + type: memory +``` + +## MCP server capabilities + +TODO: `Ibexa\Contracts\Mcp\McpCapabilityInterface` + +TODO: `Ibexa\Contracts\Mcp\Attribute` namespace + +## Example + +This example introduce an `example` MCP server with a single `greet` tool. +It's enabled on all SiteAccesses. +It's accessible with the path `/mcp/example` (for example, on `http://localhost/mcp/example` and `http://localhost/admin/mcp/example`). +It uses files for both discovery cache and session storage. + +In a new `config/packages/mcp.yaml` file, the configuration of the MCP server: + +``` yaml +[[= include_file('code_samples/mcp/config/packages/mcp.yaml') =]] +``` + +Then, a `McpCapabilityInterface`containing a `greet` function with a `McpTool` attribute associating with the `example` server: + +``` php +[[= include_file('code_samples/mcp/src/Mcp/ExampleTools.php') =]] +``` + +To check the server configuration, a short command using the MCP server configuration registry (injected through `McpServerConfigurationRegistryInterface` and autowiring): + +``` php +[[= include_file('code_samples/mcp/src/Command/McpServerListCommand.php') =]] +``` + +To test the `example` MCP server, a sequence of `curl` commands is used to simulate an AI to MCP server communication. + +- Ask for a [JWT token through REST](/api/rest_api/rest_api_reference/rest_api_reference.html#tag/User-Token/operation/api_usertokenjwt_post) +- Initialize a connection to the MCP server +- Validate the MCP Session ID +- List the available tools +- Call a tool + +`jq`, `grep`, and `sed` are also used to parse or display outputs. + +The [initialization](https://modelcontextprotocol.io/specification/draft/basic/lifecycle#initialization): + +``` bash +[[= include_file('code_samples/mcp/mcp.sh', 0, 36) =]] +``` + +``` +HTTP/1.1 202 Accepted +Access-Control-Allow-Headers: Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID, Authorization, Accept +Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS +Access-Control-Expose-Headers: Mcp-Session-Id +``` + +The [list of tools](https://modelcontextprotocol.io/specification/draft/server/tools#listing-tools): + +``` bash +[[= include_file('code_samples/mcp/mcp.sh', 37, 45) =]] +``` + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "tools": [ + { + "name": "greet", + "inputSchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "description": "Greet a user by name" + } + ] + } +} +``` + +The `greet` [tool usage](https://modelcontextprotocol.io/specification/draft/server/tools#calling-tools): + +``` bash +[[= include_file('code_samples/mcp/mcp.sh', 46) =]] +``` + +```json +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "content": [ + { + "type": "text", + "text": "Hello, World!" + } + ], + "isError": false + } +} +``` + +TODO: Connect an AI client to the MCP server. [Copilot CLI MCP server addition](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers) is strangely asking for some OAuth ID even with a proper JWT/Bearer header. diff --git a/docs/ai/mcp/mcp_guide.md b/docs/ai/mcp/mcp_guide.md new file mode 100644 index 0000000000..092fbe21ca --- /dev/null +++ b/docs/ai/mcp/mcp_guide.md @@ -0,0 +1,21 @@ +--- +description: TODO. +month_change: true +--- + +# Model Context Protocol and Ibexa MCP Servers + +[Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro) is a protocol standardizing interactions between AIs and systems. + +While [AI actions](ai_actions_guide.md) integrate AI to the back office, +[[= product_name =]]'s MCP servers offer a web interface usable by AIs outside the system. + +`ibexa/mcp` package provides built-in MCP servers and a PHP API to create custom ones. + +TODO: About built-in MCP servers (translations agents, SEO optimization agents,…) + +MCP servers capabilities (tools, prompts, and resources) can be created and associated to MCP servers thanks to a PHP API mainly based on attributes. + +MCP servers are configured per repository then enabled per SiteAccess scope, allowing for flexible configurations adapted to different contexts. + +MCP servers have their own session storage mechanism, TODO: why, benefit,… diff --git a/docs/product_guides/product_guides.md b/docs/product_guides/product_guides.md index 1c2359f5b2..5bc085b809 100644 --- a/docs/product_guides/product_guides.md +++ b/docs/product_guides/product_guides.md @@ -23,4 +23,6 @@ Discover the primary ones with the help of product guides. Condensed content all "commerce/shopping_list/shopping_list_guide", "ibexa_cloud/ibexa_cloud_guide", "cdp/cdp_guide", + "ai/ai_actions/ai_actions_guide", + "ai/mcp/mcp_guide", ], columns=4) =]] diff --git a/mkdocs.yml b/mkdocs.yml index 950afd54e4..6ee347b1e1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -346,11 +346,17 @@ nav: - Add login form: templating/layout/add_login_form.md - Add navigation menu: templating/layout/add_navigation_menu.md - Add search form to front page: templating/layout/add_search_form.md - - AI Actions: - - AI Actions: ai_actions/ai_actions.md - - AI Actions guide: ai_actions/ai_actions_guide.md - - Configure AI Actions: ai_actions/configure_ai_actions.md - - Extend AI Actions: ai_actions/extend_ai_actions.md + - AI: + - AI: ai/ai.md + - AI Actions: + - AI Actions: ai/ai_actions/ai_actions.md + - AI Actions guide: ai/ai_actions/ai_actions_guide.md + - Configure AI Actions: ai/ai_actions/configure_ai_actions.md + - Extend AI Actions: ai/ai_actions/extend_ai_actions.md + - MCP Servers: + - MCP Servers: ai/mcp/mcp.md + - MCP Servers guide: ai/mcp/mcp_guide.md + - Configure MCP Servers: ai/mcp/mcp_config.md - PIM (Product management): - PIM (Product management): pim/pim.md - PIM guide: pim/pim_guide.md diff --git a/plugins.yml b/plugins.yml index 32e5e94c06..fc5687355e 100644 --- a/plugins.yml +++ b/plugins.yml @@ -563,6 +563,11 @@ plugins: 'getting_started/install_on_ibexa_cloud.md': 'ibexa_cloud/install_on_ibexa_cloud.md' 'infrastructure_and_maintenance/clustering/ddev_and_ibexa_cloud.md': 'ibexa_cloud/ddev_and_ibexa_cloud.md' - 'ai_actions/install_ai_actions.md': 'ai_actions/configure_ai_actions.md' 'discounts/install_discounts.md': 'discounts/configure_discounts.md' 'content_management/collaborative_editing/install_collaborative_editing.md': 'content_management/collaborative_editing/configure_collaborative_editing.md' + + 'ai_actions/ai_actions.md': 'ai/ai_actions/ai_actions.md' + 'ai_actions/ai_actions_guide.md': 'ai/ai_actions/ai_actions_guide.md' + 'ai_actions/install_ai_actions.md': 'ai/ai_actions/configure_ai_actions.md' + 'ai_actions/configure_ai_actions.md': 'ai/ai_actions/configure_ai_actions.md' + 'ai_actions/extend_ai_actions.md': 'ai/ai_actions/extend_ai_actions.md'