diff --git a/code_samples/mcp/config/packages/mcp.security.yaml b/code_samples/mcp/config/packages/mcp.security.yaml new file mode 100644 index 0000000000..968e116cbb --- /dev/null +++ b/code_samples/mcp/config/packages/mcp.security.yaml @@ -0,0 +1,9 @@ +security: + firewalls: + # … + ibexa_jwt_mcp: + request_matcher: Ibexa\Mcp\Security\McpRequestMatcher + user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker + provider: ibexa + stateless: true + jwt: ~ diff --git a/code_samples/mcp/config/packages/mcp.yaml b/code_samples/mcp/config/packages/mcp.yaml new file mode 100644 index 0000000000..f71a60e119 --- /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: psr16 + directory: cache.tagaware.filesystem + system: + default: + mcp: + servers: + - example diff --git a/code_samples/mcp/http.mcp.json b/code_samples/mcp/http.mcp.json new file mode 100644 index 0000000000..f88816beb0 --- /dev/null +++ b/code_samples/mcp/http.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "ibexa-example": { + "type": "http", + "url": "http://localhost/mcp/example", + "headers": { + "Authorization": "Bearer " + }, + "tools": ["*"] + } + } +} diff --git a/code_samples/mcp/mcp-ibexa-example-wrapper.sh b/code_samples/mcp/mcp-ibexa-example-wrapper.sh new file mode 100644 index 0000000000..ce42c09439 --- /dev/null +++ b/code_samples/mcp/mcp-ibexa-example-wrapper.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e + +baseUrl='http://localhost' # Adapt to your test case + +jwtToken=$(curl -s -X 'POST' \ + "$baseUrl/api/ibexa/v2/user/token/jwt" \ + -H 'Content-Type: application/vnd.ibexa.api.JWTInput+json' \ + -H 'Accept: application/vnd.ibexa.api.JWT+json' \ + -d '{ + "JWTInput": { + "_media-type": "application/vnd.ibexa.api.JWTInput+json", + "username": "ibexa-example", + "password": "Ibexa-3xample" + } + }' | jq -r .JWT.token) + +exec npx -y supergateway \ + --streamableHttp "$baseUrl/mcp/example" \ + --oauth2Bearer "$jwtToken" \ + --logLevel none diff --git a/code_samples/mcp/mcp.matrix.yaml b/code_samples/mcp/mcp.matrix.yaml new file mode 100644 index 0000000000..fcfcc7c025 --- /dev/null +++ b/code_samples/mcp/mcp.matrix.yaml @@ -0,0 +1,43 @@ +ibexa: + repositories: + : + mcp: + : + path: + enabled: true + # Server options… + tools: + - Ibexa\Mcp\Tool\TranslationTools + - Ibexa\Mcp\Tool\SeoTools + discovery_cache: + session: + type: + # Session options… + mcp_psr16: + discovery_cache: cache.redis.mcp + session: + type: psr16 + service: cache.redis.mcp + prefix: 'mcp__' + mcp_file: + session: + type: file + directory: '%kernel.cache_dir%/mcp/sessions' + mcp_memory: + session: + type: memory + system: + : + mcp: + servers: + - +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' diff --git a/code_samples/mcp/mcp.sh b/code_samples/mcp/mcp.sh new file mode 100644 index 0000000000..27a1592f5e --- /dev/null +++ b/code_samples/mcp/mcp.sh @@ -0,0 +1,100 @@ +#!/bin/bash +set -e +set +x + +baseUrl='http://localhost' # Adapt to your test case +username='ibexa-example' +password='Ibexa-3xample' + +curl -s -X 'POST' \ + "$baseUrl/api/ibexa/v2/user/token/jwt" \ + -H 'Content-Type: application/vnd.ibexa.api.JWTInput+json' \ + -H 'Accept: application/vnd.ibexa.api.JWT+json' \ + -d "{ + \"JWTInput\": { + \"_media-type\": \"application/vnd.ibexa.api.JWTInput+json\", + \"username\": \"$username\", + \"password\": \"$password\" + } + }" > response.tmp.txt + +cat response.tmp.txt | jq +jwtToken=$(cat response.tmp.txt | jq -r .JWT.token) +rm response.tmp.txt + +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" + } + } + }' > response.tmp.txt + +sed '$d' response.tmp.txt +tail -n 1 response.tmp.txt | jq +mcpSessionId=$(cat response.tmp.txt | grep 'Mcp-Session-Id:' | sed 's/Mcp-Session-Id: \([0-9a-f-]*\).*/\1/') +rm response.tmp.txt + +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 + +curl -s -X 'POST' "$baseUrl/mcp/example" \ + -H "Authorization: Bearer $jwtToken" \ + -H "Mcp-Session-Id: $mcpSessionId" \ + -d '{ + "jsonrpc": "2.0", + "id": 4, + "method": "prompts/list" + }' | jq + +curl -s -X 'POST' "$baseUrl/mcp/example" \ + -H "Authorization: Bearer $jwtToken" \ + -H "Mcp-Session-Id: $mcpSessionId" \ + -d '{ + "jsonrpc": "2.0", + "id": 5, + "method": "prompts/get", + "params": { + "name": "greet", + "arguments": { + "name": "Firstname Lastname" + } + } + }' | jq diff --git a/code_samples/mcp/mcp.sh.output.txt b/code_samples/mcp/mcp.sh.output.txt new file mode 100644 index 0000000000..4ca7342648 --- /dev/null +++ b/code_samples/mcp/mcp.sh.output.txt @@ -0,0 +1,187 @@ +{ + "JWT": { + "_media-type": "application/vnd.ibexa.api.JWT+json", + "_token": "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz1234567890ABCD.EFGHIJKL-MNOPQRSTUVWXYZ12345678901234567890", + "token": "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz1234567890ABCD.EFGHIJKL-MNOPQRSTUVWXYZ12345678901234567890" + } +} +HTTP/1.1 200 OK +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-Allow-Origin: * +Access-Control-Expose-Headers: Mcp-Session-Id +Cache-Control: no-cache, private +Content-Type: application/json +Date: Tue, 28 Apr 2026 09:53:27 GMT +Mcp-Session-Id: 12345678-9abc-def0-1234-56789abcdef0 +Server: Apache/2.4.66 (Debian) +Vary: cookie,authorization +X-Cache-Debug: 1 +X-Debug-Token: 123456 +X-Debug-Token-Link: http://localhost/_profiler/123456 +X-Powered-By: Ibexa Commerce v5 +X-Robots-Tag: noindex +Transfer-Encoding: chunked + +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "protocolVersion": "2025-06-18", + "capabilities": { + "logging": {}, + "completions": {}, + "prompts": { + "listChanged": true + }, + "resources": { + "listChanged": true + }, + "tools": { + "listChanged": true + } + }, + "serverInfo": { + "name": "example", + "version": "1.0.0", + "description": "Example MCP Server" + }, + "instructions": "Use this server to greet someone." + } +} +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-Allow-Origin: * +Access-Control-Expose-Headers: Mcp-Session-Id +Cache-Control: no-cache, private +Content-Length: 0 +Content-Type: text/html; charset=UTF-8 +Date: Fri, 24 Apr 2026 11:16:27 GMT +Server: Apache/2.4.66 (Debian) +Vary: cookie,authorization +X-Cache-Debug: 1 +X-Debug-Token: 7890ab +X-Debug-Token-Link: http://localhost/_profiler/7890ab +X-Powered-By: Ibexa Commerce v5 +X-Robots-Tag: noindex + +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "tools": [ + { + "name": "greet", + "inputSchema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the person to greet" + } + }, + "required": [ + "name" + ] + }, + "description": "Greet a user by name", + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + }, + "icons": [ + { + "src": "https://openmoji.org/data/color/svg/1F44B.svg" + } + ], + "outputSchema": { + "type": "object", + "properties": { + "general": { + "type": "string", + "description": "the safe way to greet someone" + }, + "close": { + "type": "string", + "description": "when you're close to the person, like friends or relatives" + }, + "morning": { + "type": "string", + "description": "when it's in the morning" + }, + "afternoon": { + "type": "string", + "description": "when it's the afternoon" + }, + "evening": { + "type": "string", + "description": "when it's late in the day" + } + } + } + } + ] + } +} +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "content": [ + { + "type": "text", + "text": "{\n \"general\": \"Hello, World!\",\n \"close\": \"Hey, World!\",\n \"morning\": \"Good morning, World!\",\n \"afternoon\": \"Good afternoon, World!\",\n \"evening\": \"Good evening, World!\"\n}" + } + ], + "isError": false, + "structuredContent": { + "general": "Hello, World!", + "close": "Hey, World!", + "morning": "Good morning, World!", + "afternoon": "Good afternoon, World!", + "evening": "Good evening, World!" + } + } +} +{ + "jsonrpc": "2.0", + "id": 4, + "result": { + "prompts": [ + { + "name": "greet", + "description": "Prompt to be greeted by the `greet` tool", + "arguments": [ + { + "name": "name", + "description": "The name you want to be greeted by", + "required": true + } + ], + "icons": [ + { + "src": "https://openmoji.org/data/color/svg/1F91D.svg" + } + ] + } + ] + } +} +{ + "jsonrpc": "2.0", + "id": 5, + "result": { + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Hi. My name is Firstname Lastname. Please, greet me." + } + } + ] + } +} diff --git a/code_samples/mcp/src/Command/McpServerListCommand.php b/code_samples/mcp/src/Command/McpServerListCommand.php new file mode 100644 index 0000000000..f4deeb6de0 --- /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/ExampleCapabilities.php b/code_samples/mcp/src/Mcp/ExampleCapabilities.php new file mode 100644 index 0000000000..20568de5c3 --- /dev/null +++ b/code_samples/mcp/src/Mcp/ExampleCapabilities.php @@ -0,0 +1,91 @@ + + */ + #[McpTool( + servers: ['example'], + name: 'greet', + description: 'Greet a user by name', + annotations: new ToolAnnotations( + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + ), + icons: [new Icon( + src: 'https://openmoji.org/data/color/svg/1F44B.svg', + )], + outputSchema: [ + 'type' => 'object', + 'properties' => [ + 'general' => [ + 'type' => 'string', + 'description' => 'the safe way to greet someone', + ], + 'close' => [ + 'type' => 'string', + 'description' => 'when you\'re close to the person, like friends or relatives', + ], + 'morning' => [ + 'type' => 'string', + 'description' => 'when it\'s in the morning', + ], + 'afternoon' => [ + 'type' => 'string', + 'description' => 'when it\'s the afternoon', + ], + 'evening' => [ + 'type' => 'string', + 'description' => 'when it\'s late in the day', + ], + ], + ], + )] + public function greetByName(string $name): array + { + return [ + 'general' => sprintf('Hello, %s!', $name), + 'close' => sprintf('Hey, %s!', $name), + 'morning' => sprintf('Good morning, %s!', $name), + 'afternoon' => sprintf('Good afternoon, %s!', $name), + 'evening' => sprintf('Good evening, %s!', $name), + ]; + } + + /** + * @param string $name The name you want to be greeted by + * + * @return array + */ + #[McpPrompt( + servers: ['example'], + name: 'greet', + description: 'Prompt to invoke the `greet` tool', + icons: [new Icon( + src: 'https://openmoji.org/data/color/svg/1F91D.svg', + )], + )] + public function getGreetPrompt(string $name): array + { + return [ + 'role' => 'user', + 'content' => [ + 'type' => 'text', + 'text' => "Hi. My name is $name. Please, greet me.", + ], + ]; + } +} diff --git a/code_samples/mcp/stdio.mcp.json b/code_samples/mcp/stdio.mcp.json new file mode 100644 index 0000000000..ab195e5336 --- /dev/null +++ b/code_samples/mcp/stdio.mcp.json @@ -0,0 +1,10 @@ +{ + "mcpServers": { + "ibexa-example": { + "type": "stdio", + "command": "bash", + "args": ["mcp-ibexa-example-wrapper.sh"], + "tools": ["*"] + } + } +} diff --git a/composer.json b/composer.json index e9415fc8fd..d229c7579a 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "ibexa/core-persistence": "5.0.x-dev", "ibexa/connector-ai": "5.0.x-dev", "ibexa/connector-openai": "5.0.x-dev", + "ibexa/mcp": "5.0.x-dev", "ibexa/migrations": "5.0.x-dev", "ibexa/cart": "5.0.x-dev", "ibexa/installer": "5.0.x-dev", diff --git a/docs/ai/ai.md b/docs/ai/ai.md new file mode 100644 index 0000000000..6014973029 --- /dev/null +++ b/docs/ai/ai.md @@ -0,0 +1,22 @@ +--- +description: AI interactions with [[= product_name =]] +page_type: landing_page +month_change: true +--- + +# Artificial Intelligence + +[[= product_name =]] includes built-in AI capabilities. +For example, it can provide recommendations to product customers and content readers with the [Raptor connector](raptor_connector_guide.md), and assist editors in the back office with [AI Actions](ai_actions_guide.md). +The platform is also open to external AI integrations through [MCP (Model Context Protocol) servers](mcp_guide.md), which allow AI agents to interact with the system in a standardized way. +AI solutions are extensible. You can create [custom AI actions](extend_ai_actions.md) or expose [new MCP server capabilities](mcp_usage.md). + +AI integration goes even further: + +- Some AI agents can learn how to use the [REST](rest_api_usage.md) or [GraphQL](graphql.md) APIs. +- Other, like those integrated into IDEs, can learn how to use the [PHP API](php_api.md) and assist you in code development. + +[[= cards([ + "ai/ai_actions/ai_actions", + "ai/mcp/mcp", +], columns=2) =]] diff --git a/docs/ai_actions/ai_actions.md b/docs/ai/ai_actions/ai_actions.md similarity index 92% rename from docs/ai_actions/ai_actions.md rename to docs/ai/ai_actions/ai_actions.md index 1c02575b98..626ec0db87 100644 --- a/docs/ai_actions/ai_actions.md +++ b/docs/ai/ai_actions/ai_actions.md @@ -14,8 +14,8 @@ You can also extend it to perform other tasks or support additional AI services. ## Getting Started [[= cards([ -"ai_actions/ai_actions_guide", -"ai_actions/configure_ai_actions", +"ai/ai_actions/ai_actions_guide", +"ai/ai_actions/configure_ai_actions", ("content_management/taxonomy/taxonomy#taxonomy-suggestions", "Taxonomy suggestions", "Learn how to use AI to suggest tags and categories"), ("permissions/policies#ai-actions", "Policies", "Learn about the available AI Actions policies"), ("https://doc.ibexa.co/projects/userguide/en/5.0/ai_actions/work_with_ai_actions/"), @@ -24,7 +24,7 @@ You can also extend it to perform other tasks or support additional AI services. ## Development [[= cards([ -"ai_actions/extend_ai_actions", +"ai/ai_actions/extend_ai_actions", "api/event_reference/ai_action_events", ("https://doc.ibexa.co/en/5.0/api/rest_api/rest_api_reference/rest_api_reference.html#tag/Connector-AI", "REST API Reference", "See the available endpoints for AI Actions"), "templating/twig_function_reference/ai_actions_twig_functions", @@ -32,4 +32,3 @@ You can also extend it to perform other tasks or support additional AI services. "search/ai_actions_search_reference/action_configuration_sort_clauses", ("content_management/data_migration/importing_data#ai-action-configurations", "Importing AI actions", "Learn how to manage Action Configurations using data migrations"), ], columns=4) =]] - diff --git a/docs/ai_actions/ai_actions_guide.md b/docs/ai/ai_actions/ai_actions_guide.md similarity index 99% rename from docs/ai_actions/ai_actions_guide.md rename to docs/ai/ai_actions/ai_actions_guide.md index 614b0a85f5..33be6a9056 100644 --- a/docs/ai_actions/ai_actions_guide.md +++ b/docs/ai/ai_actions/ai_actions_guide.md @@ -115,7 +115,7 @@ Procedures are straightforward and intuitive, ensuring that users can quickly ac AI Actions feature exposes a REST API interface that allows for programmatic execution of AI actions. With the API, developers can automate tasks and execute actions on batches of content by integrating them into workflows. -For more information, see the [AI actions section in the REST API Reference](../api/rest_api/rest_api_reference/rest_api_reference.html#ai-actions-execute-ai-action). +For more information, see the [AI actions section in the REST API Reference](/api/rest_api/rest_api_reference/rest_api_reference.html#ai-actions-execute-ai-action). ## Capabilities diff --git a/docs/ai_actions/configure_ai_actions.md b/docs/ai/ai_actions/configure_ai_actions.md similarity index 100% rename from docs/ai_actions/configure_ai_actions.md rename to docs/ai/ai_actions/configure_ai_actions.md diff --git a/docs/ai_actions/extend_ai_actions.md b/docs/ai/ai_actions/extend_ai_actions.md similarity index 99% rename from docs/ai_actions/extend_ai_actions.md rename to docs/ai/ai_actions/extend_ai_actions.md index 8595bbdf7d..a6813092b0 100644 --- a/docs/ai_actions/extend_ai_actions.md +++ b/docs/ai/ai_actions/extend_ai_actions.md @@ -244,7 +244,7 @@ The Action Type options provided in the Action Context dictate whether the times ### Integrate with the REST API At this point the custom Action Type can already be executed by using the PHP API. -To integrate it with the [AI Actions execute endpoint](../api/rest_api/rest_api_reference/rest_api_reference.html#ai-actions-execute-ai-action) you need to create additional classes responsible for parsing the request and response data. +To integrate it with the [AI Actions execute endpoint](/api/rest_api/rest_api_reference/rest_api_reference.html#ai-actions-execute-ai-action) you need to create additional classes responsible for parsing the request and response data. See [adding custom media type](adding_custom_media_type.md) and [creating new REST resource](creating_new_rest_resource.md) to learn more about extending the REST API. #### Handle input data diff --git a/docs/ai_actions/img/action_handler_options.png b/docs/ai/ai_actions/img/action_handler_options.png similarity index 100% rename from docs/ai_actions/img/action_handler_options.png rename to docs/ai/ai_actions/img/action_handler_options.png diff --git a/docs/ai_actions/img/ai_actions_list.png b/docs/ai/ai_actions/img/ai_actions_list.png similarity index 100% rename from docs/ai_actions/img/ai_actions_list.png rename to docs/ai/ai_actions/img/ai_actions_list.png diff --git a/docs/ai_actions/img/ai_assistant.png b/docs/ai/ai_actions/img/ai_assistant.png similarity index 100% rename from docs/ai_actions/img/ai_assistant.png rename to docs/ai/ai_actions/img/ai_assistant.png diff --git a/docs/ai_actions/img/alt_text_use_ai.png b/docs/ai/ai_actions/img/alt_text_use_ai.png similarity index 100% rename from docs/ai_actions/img/alt_text_use_ai.png rename to docs/ai/ai_actions/img/alt_text_use_ai.png diff --git a/docs/ai_actions/img/connect_api_token.png b/docs/ai/ai_actions/img/connect_api_token.png similarity index 100% rename from docs/ai_actions/img/connect_api_token.png rename to docs/ai/ai_actions/img/connect_api_token.png diff --git a/docs/ai_actions/img/diagram_source/AI Actions.drawio b/docs/ai/ai_actions/img/diagram_source/AI Actions.drawio similarity index 100% rename from docs/ai_actions/img/diagram_source/AI Actions.drawio rename to docs/ai/ai_actions/img/diagram_source/AI Actions.drawio diff --git a/docs/ai_actions/img/guide_ai_actions.png b/docs/ai/ai_actions/img/guide_ai_actions.png similarity index 100% rename from docs/ai_actions/img/guide_ai_actions.png rename to docs/ai/ai_actions/img/guide_ai_actions.png diff --git a/docs/ai_actions/img/transcribe_audio.png b/docs/ai/ai_actions/img/transcribe_audio.png similarity index 100% rename from docs/ai_actions/img/transcribe_audio.png rename to docs/ai/ai_actions/img/transcribe_audio.png diff --git a/docs/ai/mcp/img/diagram_source/mcp-com-diagram.drawio b/docs/ai/mcp/img/diagram_source/mcp-com-diagram.drawio new file mode 100644 index 0000000000..5e1b2ba326 --- /dev/null +++ b/docs/ai/mcp/img/diagram_source/mcp-com-diagram.drawio @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/ai/mcp/img/jwt-graphiql.png b/docs/ai/mcp/img/jwt-graphiql.png new file mode 100644 index 0000000000..8bed92853b Binary files /dev/null and b/docs/ai/mcp/img/jwt-graphiql.png differ diff --git a/docs/ai/mcp/img/mcp-com-diagram.png b/docs/ai/mcp/img/mcp-com-diagram.png new file mode 100644 index 0000000000..993d078ffc Binary files /dev/null and b/docs/ai/mcp/img/mcp-com-diagram.png differ diff --git a/docs/ai/mcp/img/mcp-inspector-config.png b/docs/ai/mcp/img/mcp-inspector-config.png new file mode 100644 index 0000000000..69c2e87528 Binary files /dev/null and b/docs/ai/mcp/img/mcp-inspector-config.png differ diff --git a/docs/ai/mcp/img/mcp-inspector-greet-prompt.png b/docs/ai/mcp/img/mcp-inspector-greet-prompt.png new file mode 100644 index 0000000000..64f158f516 Binary files /dev/null and b/docs/ai/mcp/img/mcp-inspector-greet-prompt.png differ diff --git a/docs/ai/mcp/img/mcp-inspector-greet-tool.png b/docs/ai/mcp/img/mcp-inspector-greet-tool.png new file mode 100644 index 0000000000..7574f35725 Binary files /dev/null and b/docs/ai/mcp/img/mcp-inspector-greet-tool.png differ diff --git a/docs/ai/mcp/mcp.md b/docs/ai/mcp/mcp.md new file mode 100644 index 0000000000..9fe94cc241 --- /dev/null +++ b/docs/ai/mcp/mcp.md @@ -0,0 +1,17 @@ +--- +description: Overview of MCP resources in [[= product_name =]] +page_type: landing_page +edition: lts-update +month_change: true +--- + +# MCP Servers + +The Model Context Protocol (MCP) and MCP Servers allow AI agents to interact with the system in a structured way. +The feature is available as an [LTS Update](editions.md#lts-updates) since v5.0.8. + +[[= cards([ + "ai/mcp/mcp_guide", + "ai/mcp/mcp_config", + "ai/mcp/mcp_usage", +], columns=3) =]] diff --git a/docs/ai/mcp/mcp_config.md b/docs/ai/mcp/mcp_config.md new file mode 100644 index 0000000000..0ac3841cf7 --- /dev/null +++ b/docs/ai/mcp/mcp_config.md @@ -0,0 +1,180 @@ +--- +description: Configure an MCP server that exposes built-in and custom tools, prompts, and resources. +edition: lts-update +month_change: true +--- + +# Install and configure MCP Servers + +With [[= product_name =]]'s MCP Servers LTS Update package, you can expose [MCP servers](mcp_guide.md) to external AI agents. + +## Installation + +Run the following command to install the package: + +```bash +composer require ibexa/mcp +``` + +MCP Servers feature comes with [built-in tools](#built-in-tools) but doesn't come with a default configuration. +You have to create your own MCP servers by providing [their configuration](#mcp-server-configuration) and [enable JWT authentication for them](#jwt-mcp-firewall). + +## Configure authentication + +### JWT MCP firewall + +AI agents use JWT authentication against [[= product_name =]]'s MCP servers. + +In `config/packages/lexik_jwt_authentication.yaml`, [enable the `authorization_header` token extractor](development_security.md#jwt-authentication) to allow the use of JWT token bearer in `Authorization` header. + +In `config/packages/security.yaml`, make the following changes: + +- Uncomment the `ibexa_jwt_rest` firewall to enable requesting JWT tokens through REST or GraphQL API. +- Add the `ibexa_jwt_mcp` firewall to allow the use of JWT authentication against MCP servers. + +``` yaml hl_lines="4-9" +[[= include_code('code_samples/mcp/config/packages/mcp.security.yaml') =]] +``` + +!!! note "Authentication for the APIs" + + You don't need to activate JWT authentication for the REST or GraphQL API. + + For sample JWT token requests, see [REST JWT authentication](rest_api_authentication.md#jwt-authentication), [GraphQL JWT authentication](graphql.md#jwt-authentication) and [cURL test of MCP server](mcp_usage.md#perform-curl-test). + +### Repository user + +The AI agents authenticate against the MCP server with a JWT token generated for a specific repository user account. + +This repository user can be: + +- an individual user account (for example, of an editor or administrator) +- a dedicated account created specifically for AI integrations + +The repository user can generate a JWT token with their own account, or a secondary dedicated account, and pass the token to the MCP client. +A gateway could use a dedicated shared repository user to generate a JWT token and establish the connection. + +## MCP server configuration + +You define MCP servers within a repository configuration and then assign those servers to specific SiteAccess scopes. + +``` yaml +[[= include_code('code_samples/mcp/mcp.matrix.yaml', 1, 8) =]] +[[= include_code('code_samples/mcp/mcp.matrix.yaml', 12, 15) =]] +[[= include_code('code_samples/mcp/mcp.matrix.yaml', 29, 33) =]] +``` + +Routes are built automatically from MCP server `path` configs. +Those routes are identified as `ibexa.mcp.`. +You can list them by running the following command: + +`php bin/console debug:router --siteaccess= ibexa.mcp` + +### MCP server options + +| Option | Type | Required | Default | Description | +|-----------------------------------------------------------------------------------------------------------------|---------|----------|---------|------------------------------------------------------------------| +| `path` | string | Yes | | MCP server endpoint path (appended to SiteAccess-aware base URL) | +| `enabled` | boolean | No | `false` | Server state: decides whether it is enabled or disabled | +| `version` | string | No | `1.0.0` | MCP server version | +| [`description`](https://modelcontextprotocol.io/specification/2025-11-25/schema#implementation-description) | string | No | `null` | Server implementation description | +| [`instructions`](https://modelcontextprotocol.io/specification/2025-11-25/schema#initializeresult-instructions) | string | No | `null` | Prompt-like instructions provided to the AI agent | +| [`tools`](#tool-configuration) | string | No | `[]` | List of tool classes | +| [`discovery_cache`](#discovery-cache) | string | Yes | | PSR-6 or PSR-16 cache pool service identifier | +| [`session`](#session-storage) | object | Yes | | Session storage configuration | + +!!! note "New servers are disabled by default" + + After you define a server, it remains disabled until you explicitly enable it. + +### Tool configuration + +The main capabilities of an MCP server are called [tools](https://modelcontextprotocol.io/specification/latest/server/tools). +They are the actions that an AI agent can invoke on the system. + +!!! note "MCP server design best practices" + + Avoid creating MCP servers with large tool sets. + Too many tools make it more difficult for the AI agent to select the appropriate action. + Instead, create multiple MCP servers with specific sets of tools dedicated to specific contexts or use cases. + When designing MCP servers, focus on the needs and tasks of the human user who actually interacts with the AI agent rather than exploring every technical capability. + +There are two ways to associate tools with a server: + +- By listing PHP classes (FQCNs) in the server's configuration `tools`. All tools marked with the `McpTool` attribute in those classes are automatically associated with the server (for example, for [built-in](#built-in-tools) or third party tools). +- By using the `servers` argument in [`McpTool` attribute](mcp_usage.md#tools) to explicitly associate a specific tool with MCP servers. + +#### Built-in tools + +MCP Servers LTS Update comes with the following built-in tools: + +- `Ibexa\Mcp\Tool\TranslationTools` + - `list_languages` - lists all languages in the current SiteAccess + - `list_content_translations` - lists languages in which given content item has translations +- `Ibexa\Mcp\Tool\SeoTools` + - `get_non_seo_content_ids` - returns IDs of content items that are missing SEO optimization (no meta title tag) + +``` yaml hl_lines="5-7" +[[= include_code('code_samples/mcp/mcp.matrix.yaml', 4, 7) =]] +[[= include_code('code_samples/mcp/mcp.matrix.yaml', 9, 11) =]] + # … +``` + +### Discovery cache + +Discovery is cached to avoid scanning for capabilities on every request. +You must provide a PSR-6 or PSR-16 cache pool for this caching. + +For example, you could set up a dedicated Redis/Valkey: + +``` yaml +[[= include_code('code_samples/mcp/mcp.matrix.yaml', 17, 17) =]] +``` + +For a production cluster, it's recommended to use a Redis/Valkey cache pool so the cache can be shared by all nodes. + +Clear the cache pool after making changes: + +```bash +php bin/console cache:pool:clear cache.redis.mcp +``` + +### Session storage + +MCP servers store session data in their own way. + +#### Options + +| Option | Type | Default | Description | +|-------------|---------|------------|-----------------------------------------------------------| +| `type` | enum | (required) | Session store type: [`psr16`](#psr-16) or [`file`](#file) | +| `service` | string | `null` | PSR-16 cache service ID for the `psr16` session store | +| `prefix` | string | `mcp_` | Key prefix for the `psr16` session store | +| `directory` | string | `null` | Directory path for the `file` session store | +| `ttl` | integer | `3600` | Session TTL in seconds | + +In production, it’s recommended to use [`psr16`](#psr-16) with Redis/Valkey, like with [regular sessions](clustering.md#shared-sessions). + +#### PSR-16 + +Sessions are stored with a PSR-16 compatible cache implementation. +It requires that a `service` option points to a valid cache service ID. +Optionally, you could use a more specific `prefix` option than the default `mcp_` to avoid key collisions with other cache usages. +Such setup is suitable for production environments. + +``` yaml +[[= include_code('code_samples/mcp/mcp.matrix.yaml', 18, 21) =]] +[[= include_code('code_samples/mcp/mcp.matrix.yaml', 34, 43) =]] +``` + +#### File + +Sessions are stored on the filesystem. +This requires that you configure a directory. +Such setup is suitable for development environments. + +In this example, sessions are stored in the `var/cache//mcp/sessions/` directory (for example, `var/cache/dev/mcp/session/` for the `dev` environment, and `var/cache/prod/mcp/sessions/` for the `prod` environment): + +``` yaml +[[= include_code('code_samples/mcp/mcp.matrix.yaml', 23, 25) =]] +``` diff --git a/docs/ai/mcp/mcp_guide.md b/docs/ai/mcp/mcp_guide.md new file mode 100644 index 0000000000..3d6c3f117f --- /dev/null +++ b/docs/ai/mcp/mcp_guide.md @@ -0,0 +1,43 @@ +--- +description: MCP servers expose tools, specialized prompts, and resources to AI agents. +edition: lts-update +month_change: true +--- + +# MCP Servers product guide + + +## What is MCP Servers + +MCP ([Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)) is a protocol that standardizes how AI systems interact with external systems. + +While [AI actions](ai_actions_guide.md) integrate AI with the back office, +[[= product_name =]]'s [MCP Servers](https://modelcontextprotocol.io/docs/learn/server-concepts) offer an API that can be used by AI agents from the outside of the system. + +Because MCP is a standard protocol, many agents are already trained to use it. + +They can interact directly with REST or GraphQL APIs if their users provide detailed instructions through prompts, skill files, etc. +However, when facing a specific REST or GraphQL API, an agent may misunderstand the purpose of endpoints, hallucinate paths, or send incorrectly structured parameters. + +MCP servers make the discovery of available capabilities much easier. +They help AI agents translate natural language prompts into concrete actions on the system. + +![](img/mcp-com-diagram.png) + +An MCP server allows the agent to discover available tools, inspect their parameters, learn how to use them, and select the correct action. + +## Availability + +MCP Servers feature is an [LTS Update package](editions.md#lts-updates) available starting with the v5.0.8 in all Ibexa DXP editions. + +## Capabilities + +With the MCP Servers feature, you can: + +- create MCP servers [by using YAML configuration](mcp_config.md#mcp-server-configuration) +- assign different tools, prompts, and resources to different MCP servers, varying them for each site and purpose +- use [built-in tools](mcp_config.md#built-in-tools) included in the package +- [create custom server capabilities](mcp_usage.md#create-capability-class) with PHP API + +MCP servers are defined specifically for each [repository](repository_configuration.md) and assigned to individual [SiteAccesses](siteaccess.md) scopes. +This way you can build flexible configurations that match different contexts. diff --git a/docs/ai/mcp/mcp_usage.md b/docs/ai/mcp/mcp_usage.md new file mode 100644 index 0000000000..954361c22f --- /dev/null +++ b/docs/ai/mcp/mcp_usage.md @@ -0,0 +1,417 @@ +--- +description: Create custom capabilities for your MCP servers and test them. +edition: lts-update +month_change: true +--- + +# Work with MCP servers + +The MCP Servers [LTS Update](editions.md#lts-updates) includes several [built-in tools](mcp_config.md#built-in-tools). +Additionally, you can create your own capabilities (tools, prompts, and resources) to expose custom features to AI agents through your MCP servers. + +## MCP server capabilities + +The [[= product_name =]] MCP server framework (`ibexa/mcp`) is built on top of the [official PHP SDK for MCP (`mcp/sdk`)](https://github.com/modelcontextprotocol/php-sdk). + +A PHP class that implements MCP server capabilities such as tools, prompts, or resources, must: + +- implement [`Ibexa\Contracts\Mcp\McpCapabilityInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Mcp-McpCapabilityInterface.html) so that it can be scanned for capabilities +- use attributes from the [`Ibexa\Contracts\Mcp\Attribute` namespace](/api/php_api/php_api_reference/namespaces/ibexa-contracts-mcp-attribute.html) to declare capabilities + +### Tools + +The [`Ibexa\Contracts\Mcp\Attribute\McpTool` attribute](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Mcp-Attribute-McpTool.html) declares a method as an MCP tool. +It accepts the following optional arguments: + +- `servers` - array of server identifiers the tool is assigned to +
For more information, see [tools configuration](mcp_config.md#tool-configuration). +- `name` - tool name (if not set, function name is used) +- `description` - tool description, used by AI agents to understand the tool's purpose +- `icons` - array of [`Mcp\Schema\Icon`](https://github.com/modelcontextprotocol/php-sdk/blob/main/src/Schema/Icon.php) instances +
For more information, see the [`icons` specification](https://modelcontextprotocol.io/specification/latest/basic/index#icons). +- `outputSchema` - associative array describing a JSON object response +- `annotations` - [`Mcp\Schema\ToolAnnotations`](https://github.com/modelcontextprotocol/php-sdk/blob/main/src/Schema/ToolAnnotations.php) instance +
For more information, see the [`ToolAnnotations` specification](https://modelcontextprotocol.io/specification/2025-11-25/schema#toolannotations). +- `meta` - free-form array for additional metadata +
For more information, see the [`_meta` specification](https://modelcontextprotocol.io/specification/latest/basic/index#_meta). + +The framework automatically builds an `inputSchema` from the method arguments and their types. +To customize or extend the generated schema, you can: + +- add descriptions with DocBlock `@param` tags +- use the [`Schema` attribute](https://github.com/php-mcp/server#-schema-generation-and-validation) + +If an argument is an [enum](https://www.php.net/manual/en/language.types.enumerations.php), its possible values are listed in the schema ([`UntitledSingleSelectEnumSchema`](https://modelcontextprotocol.io/specification/latest/schema#untitledsingleselectenumschema)). + +### Prompts + +MCP servers can also provide [prompt templates](https://modelcontextprotocol.io/specification/latest/server/prompts) to help users interact with AI agents connected to the server. + +Methods that return a prompt are marked with the [`Ibexa\Contracts\Mcp\Attribute\McpPrompt` attribute](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Mcp-Attribute-McpTool.html). + +It accepts several arguments that describe how the prompt is used: + +- `servers` - array of server identifiers exposing this prompt - required for prompts +- `name` (optional) - prompt name - if not set, method name is used +- `description` (optional) - human-readable prompt description +- `icons` (optional) - array of [`Mcp\Schema\Icon`](https://github.com/modelcontextprotocol/php-sdk/blob/main/src/Schema/Icon.php) instances +
For more information, see the [`icons` specification](https://modelcontextprotocol.io/specification/latest/basic/index#icons). +- `meta` (optional) - rarely used free-form array for additional metadata +
For more information, see the [`_meta` specification](https://modelcontextprotocol.io/specification/latest/basic/index#_meta). + +The framework automatically builds the `arguments` array from the method arguments and their types. +Prompt method arguments must be strings to comply with the [`GetPromptRequestParams` schema](https://modelcontextprotocol.io/specification/latest/schema#getpromptrequestparams). +To add argument descriptions, use DocBlock `@param` tags, it's mapped to the `description` defined by the [`PromptArgument` schema](https://modelcontextprotocol.io/specification/latest/schema#promptargument). + +## Example + +To keep the example focused on MCP server configuration and capability creation, it doesn't interact with the [[= product_name =]] repository. + +### Create user account + +In this example, the MCP server uses JWT tokens created with a dedicated user account. + +In [[= product_name =]]'s back office, create a user in the **Guest accounts** user group, with login `ibexa-example` and password `Ibexa-3xample`. + +### Configure MCP server + +This example introduces an MCP server named `example`, with a single tool called `greet`. +The server: + +- is enabled on the default repository +- is available in all SiteAccesses +- is accessible with the path `/mcp/example` +
For example: + - `http://localhost/mcp/example` + - `http://localhost/admin/mcp/example` +- uses file storage for both discovery cache and sessions + +!!! note "Storage choice recommendations" + + Filesystem storage is convenient for the sake of this example and for testing. + For production, it is recommended that you use Redis or Valkey. + +In a new `config/packages/mcp.yaml` file, define a new MCP server for the `default` repository and assign it to all SiteAccesses: + +``` yaml +[[= include_code('code_samples/mcp/config/packages/mcp.yaml') =]] +``` + +An `ibexa.mcp.example` route is now available: + +```bash +php bin/console debug:router ibexa.mcp.example +``` + +### Create capability class + +Create an `ExampleCapabilities` class that implements `McpCapabilityInterface`. + +The class contains: + +- a method marked with an [`McpTool` attribute](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Mcp-Attribute-McpTool.html) that associates it to the `example` server as `greet` tool +- a method marked with an [`McpPrompt` attribute](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Mcp-Attribute-McpPrompt.html) that provides a prompt template to users + +``` php +[[= include_code('code_samples/mcp/src/Mcp/ExampleCapabilities.php') =]] +``` + +In this example, the `servers` attribute parameter associates only this tool with the `example` server. +Alternatively, you can assign all tools from the class to a server by using the `tools` parameter in server configuration. +For more information, see [tools configuration](mcp_config.md#tool-configuration). + +For the prompt, the `servers` parameter is required. +Therefore, the example prompt must use it to be associated with the `example` server. + +During development and testing, you may need to clear the cache to ensure that new or modified capabilities are properly re-discovered. +In this example, use the following command: + +```bash +php bin/console cache:pool:clear cache.tagaware.filesystem +``` + +!!! tip "Cache clearing" + + During development, clear caches aggressively. + The following commands clear all cache types, regardless of where they are stored: + ```bash + php bin/console cache:clear + php bin/console cache:pool:clear --all + ``` + +### Create MCP server list command + +To check the MCP server configuration, create a small command that uses the MCP server configuration registry injected through [`McpServerConfigurationRegistryInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Mcp-McpServerConfigurationRegistryInterface.html) and autowiring: + +``` php +[[= include_code('code_samples/mcp/src/Command/McpServerListCommand.php') =]] +``` + +### Perform `curl` test + +To test the `example` MCP server, a sequence of `curl` commands is used to simulate the communication between an AI client and the MCP server. + +- 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. + +First, use the shell script to set the [[= product_name =]]'s base URL and user credentials as variables for easier reuse: + +``` bash +[[= include_code('code_samples/mcp/mcp.sh', 5, 7) =]] +``` + +Before you can communicate with the MCP server, you must first request a JWT token through the REST API: + +``` bash +[[= include_code('code_samples/mcp/mcp.sh', 9, 23) =]] +``` + +``` json +[[= include_code('code_samples/mcp/mcp.sh.output.txt', 1, 7) =]] +``` + +Then, perform [initialization](https://modelcontextprotocol.io/specification/latest/basic/lifecycle#initialization) to get an MCP session ID: + +``` bash +[[= include_code('code_samples/mcp/mcp.sh', 21, 44) =]] +``` + +``` http +[[= include_code('code_samples/mcp/mcp.sh.output.txt', 8, 16) =]] +``` + +``` json +[[= include_code('code_samples/mcp/mcp.sh.output.txt', 26, 51) =]] +``` + +Validate the initialization: + +``` bash +[[= include_code('code_samples/mcp/mcp.sh', 46, 52) =]] +``` + +``` http +[[= include_code('code_samples/mcp/mcp.sh.output.txt', 52, 56) =]] +``` + +Get the [list of tools](https://modelcontextprotocol.io/specification/latest/server/tools#listing-tools): + +``` bash +[[= include_code('code_samples/mcp/mcp.sh', 54, 61) =]] +``` + +``` json +[[= include_code('code_samples/mcp/mcp.sh.output.txt', 69, 128) =]] +``` + +[Call](https://modelcontextprotocol.io/specification/latest/server/tools#calling-tools) the `greet` tool: + +``` bash +[[= include_code('code_samples/mcp/mcp.sh', 63, 76) =]] +``` + +``` json +[[= include_code('code_samples/mcp/mcp.sh.output.txt', 129, 148) =]] +``` + +Get the [list of prompts](https://modelcontextprotocol.io/specification/latest/server/prompts#listing-prompts): + +``` bash +[[= include_code('code_samples/mcp/mcp.sh', 78, 85) =]] +``` + +``` json +[[= include_code('code_samples/mcp/mcp.sh.output.txt', 149, 172) =]] +``` + +[Get the prompt](https://modelcontextprotocol.io/specification/2025-11-25/server/prompts#getting-a-prompt) of the `greet` method: + +``` bash +[[= include_code('code_samples/mcp/mcp.sh', 87, 100) =]] +``` + +``` json +[[= include_code('code_samples/mcp/mcp.sh.output.txt', 173, 187) =]] +``` + +### Perform MCP Inspector test + +You can test your server with the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector). +You can even use the inspector as a DDEV add-on with [`craftpulse/ddev-mcp-inspector`](https://github.com/craftpulse/ddev-mcp-inspector). +You still need to ask for a JWT token through REST or GraphQL APIs, and use it in the MCP Inspector configuration to connect to the server. + +You can use a Web interface to obtain the JWT token: + +- [REST live documentation](rest_api_authentication.md#jwt-token-obtained-through-rest-documentation) +- [GraphiQL](graphql.md#jwt-authentication) + +#### MCP server settings + +In this example, the settings needed to use the MCP Inspector are as follows: + +- Transport Type: Streamable HTTP +- URL: actual domain and server `path`, for example `http://localhost/mcp/example` +- Connection Type: Via Proxy +- Authentication: + - Custom Headers: + - `Authorization` + - `Bearer ` + - OAuth 2.0 Flow: left unedited + +![Left panel of MCP Inspector with connection settings for MCP server](img/mcp-inspector-config.png "MCP Inspector connection settings") + +#### Test MCP server within MCP Inspector + +In the right panel, in the **Tools** tab, click **List Tools** in the left column. +The `greet` tool appears, preceded by its icon. +You can select and test it in the right column. + +![Right panel of MCP Inspector with a list of tools obtained from MCP server, and the test of the `greet` tool](img/mcp-inspector-greet-tool.png "MCP Inspector `greet` tool test") + +In the **Prompts** tab, in the left column, click **List Prompts**. +The `greet` prompt appears, preceded by its icon. +You can select and test it in the right column. + +![Right panel of MCP Inspector with a list of prompts obtained from the MCP server, and the test of the `greet` prompt](img/mcp-inspector-greet-prompt.png "MCP Inspector `greet` prompt test") + +### Perform Copilot CLI test + +#### Add MCP server to Copilot CLI + +For the sake of the [Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/copilot-cli/about-copilot-cli) test in this example, you configure the MCP server in an `.mcp.json` file at the [[= product_name =]] project root. +This way it is only available for a session opened from there. + +You can handle the JWT token for this test in the following ways: + +- Hard code the JWT token into the configuration and update it at every expiration. +- Wrap a JWT token request and an MCP server call into a script. + +##### Hard coded variant + +The hard coded JWT token configuration in `.mcp.json` looks as follows: + +``` json +[[= include_code('code_samples/mcp/http.mcp.json') =]] +``` + +The `.mcp.json` file must be edited to update the JWT token each time it expires. +You can request a token by using the GraphiQL web interface or a `curl` command, and then edit the file manually. +Alternatively, you can configure a shell script to request the JWT token, extract it from the response, and replace it in the file. + +When Copilot complains that it can't communicate with the MCP server: + +- Update the JWT token in the `.mcp.json` file. +- Reload the MCP servers in Copilot CLI with one of these methods: + - Run `/mcp reload` command to reload all MCP servers. + - Run `/mcp disable ibexa-example` and `/mcp enable ibexa-example` to only reload the `ibexa-example` server. + +##### Fully scripted variant + +The wrapping script configuration in `.mcp.json` looks as follows: + +``` json +[[= include_code('code_samples/mcp/stdio.mcp.json') =]] +``` + +`mcp-ibexa-example-wrapper.sh` is a script that requests a JWT token and establishes a connection with the MCP server. + +For example, thanks to [`npx`](https://www.npmjs.com/package/npx), you can do it with [Supergateway](https://www.npmjs.com/package/supergateway) without a local installation: + +``` bash +[[= include_code('code_samples/mcp/mcp-ibexa-example-wrapper.sh') =]] +``` + +When Copilot complains that it can't communicate with the MCP server, reload the MCP servers in Copilot CLI with one of these methods: + +- Run `/mcp reload` command to reload all MCP servers. +- Run `/mcp disable ibexa-example` and `/mcp enable ibexa-example` to only reload the `ibexa-example` server. + +!!! note "Reloading multiple MCP servers" + + If you have several MCP servers enabled globally, reloading all of them at the same time can be time consuming. + Consider reloading them one by one. + +#### Run MCP server test with Copilot CLI + +Launch Copilot CLI at the project root, where the `.mcp.json` file is located: + +```bash +cd /path/to/project +copilot +``` + +If prompted, confirm that you trust the files in this folder. +You may choose to have your choice remembered for the future. + +Run the `/mcp show ibexa-example` to check the MCP server status and details: + +```text + MCP Server: ibexa-example + + Type: http + URL: http://localhost/mcp/example + Status: ✓ Connected + Source: /path/to/project/.mcp.json + + Tools (1/1 enabled): + ✓ greet: Greet a user by name +``` + +You can prompt Copilot to greet you. +It should use the `greet` tool to do so. + +- You can ask it "Please, greet me." and it might respond with a request for your name if it doesn't already know it. +- You can also give it another name to greet, like in the prompt template. + +During a morning session, the interaction could look like this: + +``` +❯ Hi. Please, greet me. + +◐ The user wants to be greeted. I'll use the ibexa-example-greet tool, but I don't know their name. Let me ask them first... actually, the tool requires a name. Let me ask the user for their name. + + Wait, actually I can just greet them without knowing their name using a general greeting, but the tool requires a name parameter. Let me ask for their name. + +● Asked user What's your name? + └ Pulley + +● Greet a user (MCP: ibexa-example) · name: "Pulley" + └ {"general":"Hello, Pulley!","close":"Hey, Pulley!","morning":"Good morning, P... + +● Good morning, Pulley! ☀️ + +❯ Hi. Please, greet me. We're close friends. + +◐ The user wants a close friend greeting. From the previous response, I can see the "close" greeting is "Hey, Pulley!" + +● Greet a user (MCP: ibexa-example) · name: "Pulley" + └ {"general":"Hello, Pulley!","close":"Hey, Pulley!","morning":"Good morning, P... + +● Hey, Pulley! 😄 + +❯ Please, greet someone named "ElePHPant" in a general way (so I could reuse it at any time) + +◐ The user wants a general greeting for someone named "ElePHPant". + +● greet (MCP: ibexa-example) · name: "ElePHPant" + └ {"general":"Hello, ElePHPant!","close":"Hey, ElePHPant!","morning":"Good morn... + +● Hello, ElePHPant! 🐘 + +❯ Please, greet someone named "ElePHPant" like it's 22:30 + +◐ 22:30 is evening time, so I'll use the evening greeting. + +● greet (MCP: ibexa-example) · name: "ElePHPant" + └ {"general":"Hello, ElePHPant!","close":"Hey, ElePHPant!","morning":"Good morn... + +● Good evening, ElePHPant! 🌙 +``` + +The Copilot's reflection and its final response, including the improvised emoji, may differ from this example. +The key point is that Copilot CLI decides to use the `greet` tool, calls it with the right argument, displays the call result, and then uses it in its final output. diff --git a/docs/api/api.md b/docs/api/api.md index 813a5edae7..7d3cf2ede9 100644 --- a/docs/api/api.md +++ b/docs/api/api.md @@ -7,8 +7,17 @@ page_type: landing_page [[= product_name =]] is an API-first product and provides APIs to handle content and repository information. +## Web API + [[= cards([ "api/rest_api/rest_api_usage/rest_api_usage", - "api/php_api/php_api", "api/graphql/graphql", + "ai/mcp/mcp", +], columns=3) =]] + +## PHP API + +[[= cards([ + "api/php_api/php_api", + "api/event_reference/event_reference", ], columns=3) =]] diff --git a/docs/api/graphql/graphql.md b/docs/api/graphql/graphql.md index d9bc9466ef..47e665b8c6 100644 --- a/docs/api/graphql/graphql.md +++ b/docs/api/graphql/graphql.md @@ -94,6 +94,27 @@ Response: } ``` +#### JWT token obtained through GraphiQL + +To obtain a JWT token, you can use the GraphiQL interface on your development installation. +GraphiQL is only accessible when `kernel.debug` is set to `true`, similarly to a development environment. + +- open GraphiQL UI (for example, at `http://localhost/graphiql`) +- paste in the following adapted query with the user credentials +- click the execute button **▶** to get a token + +```graphql +mutation CreateToken { + createToken(username: "ibexa-example", password: "Ibexa-3xample") { + token + message + } +} +``` + +![GraphiQL with a JWT token request and its response](jwt-graphiql.png "GraphiQL JWT token request and response") + + ## Usage You can access GraphQL with `/graphql`. diff --git a/docs/api/rest_api/img/jwt-rest-doc-request.png b/docs/api/rest_api/img/jwt-rest-doc-request.png new file mode 100644 index 0000000000..dc171e7910 Binary files /dev/null and b/docs/api/rest_api/img/jwt-rest-doc-request.png differ diff --git a/docs/api/rest_api/img/jwt-rest-doc-response.png b/docs/api/rest_api/img/jwt-rest-doc-response.png new file mode 100644 index 0000000000..0d3ee9aa52 Binary files /dev/null and b/docs/api/rest_api/img/jwt-rest-doc-response.png differ diff --git a/docs/api/rest_api/rest_api_authentication.md b/docs/api/rest_api/rest_api_authentication.md index f7f77366f8..bfe112d350 100644 --- a/docs/api/rest_api/rest_api_authentication.md +++ b/docs/api/rest_api/rest_api_authentication.md @@ -276,45 +276,57 @@ See [JWT authentication](development_security.md#jwt-authentication) for configu After you configure JWT authentication for REST, you can get the JWT token through the following request: -=== "JSON" - - ``` - POST /user/token/jwt HTTP/1.1 - Host: - Accept: application/vnd.ibexa.api.JWT+json - Content-Type: application/vnd.ibexa.api.JWTInput+json - ``` +```http +POST /user/token/jwt HTTP/1.1 +Host: +Accept: application/vnd.ibexa.api.JWT+json +Content-Type: application/vnd.ibexa.api.JWTInput+json +``` - Provide the username and password in the request body: +Provide the username and password in the request body: - ```json - { - "JWTInput": { - "username": "admin", - "password": "publish" - } +```json +{ + "JWTInput": { + "username": "admin", + "password": "publish" } - ``` +} +``` - If credentials are valid, the server response contains a token: +If credentials are valid, the server response contains a token: - ```json - { - "JWT": { - "_media-type": "application/vnd.ibexa.api.JWT+xml", - "_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…-QBE4-6eKNjg" - } +```json +{ + "JWT": { + "_media-type": "application/vnd.ibexa.api.JWT+xml", + "_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…-QBE4-6eKNjg" } - ``` +} +``` - You can then use this token in your request instead of username and password. +You can then use this token in your request instead of username and password. - ``` - GET /content/locations/1/5/children - Host: - Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…-QBE4-6eKNjg - Accept: application/vnd.ibexa.api.LocationList+json - ``` +```http +GET /content/locations/1/5/children +Host: +Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…-QBE4-6eKNjg +Accept: application/vnd.ibexa.api.LocationList+json +``` + +#### JWT token obtained through REST documentation + +To obtain a JWT token with REST, you can use the live API documentation that is available on your development installation. +This documentation is only accessible when `kernel.debug` is set to `true`, similarly to a development environment. + +- open REST API live doc (for example at `http://localhost/api/ibexa/v2/doc`) +- go to **User Token** section's **POST /user/token/jwt** resource (for example, at `http://localhost/api/ibexa/v2/doc#/User%20Token/api_usertokenjwt_post`) +- click the **Try it out** button +- fill in the following adapted payload with the user credentials +- click the **Execute** button to get a token + +![REST API live documentation with a JWTInput payload](jwt-rest-doc-request.png "REST doc JWT token request") +![REST API live documentation with a JWTInput payload](jwt-rest-doc-response.png "REST doc JWT token response") ## HTTP basic authentication diff --git a/docs/ibexa_products/editions.md b/docs/ibexa_products/editions.md index 718f3b0266..fc5296196e 100644 --- a/docs/ibexa_products/editions.md +++ b/docs/ibexa_products/editions.md @@ -69,4 +69,5 @@ The features brought by LTS Updates become standard parts of the next LTS releas | [Anthropic connector](configure_ai_actions.md#install-anthropic-connector) | ✔ | ✔ | ✔ | | [Google Gemini connector](configure_ai_actions.md#install-google-gemini-connector) | ✔ | ✔ | ✔ | | [Integrated help](integrated_help.md) | ✔ | ✔ | ✔ | +| [MCP servers](mcp_guide.md) | ✔ | ✔ | ✔ | | [Shopping list](shopping_list_guide.md) | | | ✔ | diff --git a/docs/index.md b/docs/index.md index 0bd8b52644..591499ccf1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -89,11 +89,11 @@
-

The newest LTS Update is Google Gemini connector

-
Integrate Google's AI services into Ibexa DXP.
+

The newest LTS Update is MCP Servers

+
Set up MCP servers to help AI agents interact with Ibexa DXP.
diff --git a/docs/infrastructure_and_maintenance/security/development_security.md b/docs/infrastructure_and_maintenance/security/development_security.md index 098edb0175..a93e3abc29 100644 --- a/docs/infrastructure_and_maintenance/security/development_security.md +++ b/docs/infrastructure_and_maintenance/security/development_security.md @@ -119,8 +119,8 @@ lexik_jwt_authentication: enabled: false ``` -You also need a new Symfony firewall configuration for REST and/or GraphQL APIs. -It's already provided in `config/packages/security.yaml`, you only need to uncomment it: +You also need to configure Symfony firewalls for the APIs with which you want to use JWT authentication. +It's already provided in `config/packages/security.yaml`, you need to uncomment the `ibexa_jwt_rest` and the ones for the desired APIs: ``` yaml security: @@ -151,16 +151,57 @@ security: jwt: ~ ``` -Finish the setup by generating a [PEM encoded key pair](https://symfony.com/bundles/LexikJWTAuthenticationBundle/2.x/index.html#generate-the-ssl-keys) by using the command: +- `ibexa_jwt_rest` is the firewall that allows to generate a JWT token through REST or GraphQL +- `ibexa_jwt_rest.api` is the firewall to [use JWT authentication for REST API](rest_api_authentication.md#jwt-authentication) instead of session-based +- `ibexa_jwt_graphql` is the firewall to [use JWT authentication for GraphQL API](graphql.md#jwt-authentication) + +For example, to use JWT authentication only for GraphQL API and keep session-based authentication for REST API: + +- uncomment `ibexa_jwt_rest` and `ibexa_jwt_graphql` to activate them +- keep `ibexa_jwt_rest.api` commented and disabled + +### Use PEM keys + +Out of the box, JWT tokens are created by using HMAC (Hash-based Message Authentication Code) with `APP_SECRET` as the secret key and the `HS256` (HMAC-SHA256) algorithm. + +You can use PEM (Privacy-enhanced Electronic Mail) keys and the `RS256` (RSA-SHA256) algorithm instead. + + +1. Set the `JWT_PASSPHRASE` secret + +In an `.env` file, you should have the following variables: + +```dotenv +JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem +JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem +JWT_PASSPHRASE=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ…… +``` + +Set your `JWT_PASSPHRASE`, its value must be strong, random, and securely stored. +For more recommendations and to learn how to generate one, see [`APP_SECRET` and other secrets](security_checklist.md#app_secret-and-other-secrets). + +2. In `config/packages/lexik_jwt_authentication.yaml`, use the following configuration: + +``` yaml hl_lines="2-4 6" +lexik_jwt_authentication: + secret_key: '%env(resolve:JWT_SECRET_KEY)%' + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + pass_phrase: '%env(JWT_PASSPHRASE)%' + encoder: + signature_algorithm: RS256 + # … +``` + +3. Generate a [PEM encoded key pair](https://symfony.com/bundles/LexikJWTAuthenticationBundle/2.x/index.html#generate-the-ssl-keys) by using the following command which outputs key files in the `config/jwt` directory: ```bash php bin/console lexik:jwt:generate-keypair ``` -The generated key pair will be stored in the `config/jwt`directory. - !!! note "[[= product_name_cloud =]]" - To generate and store the tokens on [[= product_name_cloud =]], define the `config/jwt` directory as a volume in the `.platform.app.yaml` file. + To store the tokens on [[= product_name_cloud =]], define the `config/jwt` directory as a volume in the `.platform.app.yaml` file. In 3-node cluster setups, ensure that the key pair is the same on all 3 servers. You can use a network share, or use a local mount and manually copy the key pair between the servers. + +For more information, see [LexikJWTAuthenticationBundle configuration reference](https://symfony.com/bundles/LexikJWTAuthenticationBundle/2.x/1-configuration-reference.html). diff --git a/docs/infrastructure_and_maintenance/security/security_checklist.md b/docs/infrastructure_and_maintenance/security/security_checklist.md index 1f34f7efc6..638f689dc6 100644 --- a/docs/infrastructure_and_maintenance/security/security_checklist.md +++ b/docs/infrastructure_and_maintenance/security/security_checklist.md @@ -151,6 +151,13 @@ This means that editors who have access to Code blocks could add malicious JS in As site administrator, be aware of this when giving editors access to the Page Builder features, and limit that access only to trusted editors. You can [limit access to specific blocks per content type]([[= user_doc =]]/content_management/configure_ct_field_settings/#default-configuration-of-pages) by defining which page blocks are available to editors. +### Activate JWT authentication for MCP, REST, or GraphQL + +To use [MCP servers](mcp_guide.md), you must enable JWT authentication for them. +You can also consider enabling JWT authentication for [REST](rest_api_usage.md) or [GraphQL](graphql.md) APIs. + +For more information, see [Development security](development_security.md#jwt-authentication). + ## Symfony ### `APP_SECRET` and other secrets diff --git a/docs/product_guides/product_guides.md b/docs/product_guides/product_guides.md index 0a0072b99f..4516c3ea34 100644 --- a/docs/product_guides/product_guides.md +++ b/docs/product_guides/product_guides.md @@ -25,4 +25,6 @@ Discover the primary ones with the help of product guides. Condensed content all "ibexa_cloud/ibexa_cloud_guide", "cdp/cdp_guide", "recommendations/raptor_integration/raptor_connector_guide", + "ai/ai_actions/ai_actions_guide", + "ai/mcp/mcp_guide", ], columns=4) =]] diff --git a/mkdocs.yml b/mkdocs.yml index ca8e299822..6c7994b39a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -353,11 +353,18 @@ 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 + - Install MCP Servers: ai/mcp/mcp_config.md + - Work with MCP servers: ai/mcp/mcp_usage.md - Product catalog: - Product catalog: product_catalog/product_catalog.md - Product catalog guide: product_catalog/product_catalog_guide.md @@ -643,7 +650,7 @@ nav: - SymbolAttribute: search/criteria_reference/symbolattribute_criterion.md - UpdatedAt: search/criteria_reference/updated_at_criterion.md - UpdatedAtRange: search/criteria_reference/updated_at_range_criterion.md - - Order Search Criteria: + - Order Search Criteria: - Order Search Criteria: search/criteria_reference/order_search_criteria.md - CompanyName: search/criteria_reference/order_company_name_criterion.md - CreatedAt: search/criteria_reference/order_created_criterion.md diff --git a/plugins.yml b/plugins.yml index 77c3fd4879..4f1cc26481 100644 --- a/plugins.yml +++ b/plugins.yml @@ -583,6 +583,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'