feat: add MCP server for the website chatbot over a mirrored TYPO3 content feed

This commit is contained in:
fromme committed 2026-10-08 11:46:58 +02:00
1 parent ff7ddf7a9e
commit 1b4384e3f3
52 files changed
+5117 -4

No files matched your search

+1
View File
@@ -20,4 +20,5 @@ return [
Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true],
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
Symfony\AI\McpBundle\McpBundle::class => ['all' => true],
];
+7
View File
@@ -16,6 +16,13 @@ framework:
php_errors:
log: true
rate_limiter:
# Per MCP token. A chatbot turn makes a few tool calls; this catches runaway loops.
chatbot_mcp:
policy: sliding_window
limit: 60
interval: '1 minute'
http_client:
scoped_clients:
typo3.client:
+10
View File
@@ -0,0 +1,10 @@
services:
Psr\Http\Message\RequestFactoryInterface: '@http_discovery.psr17_factory'
Psr\Http\Message\ResponseFactoryInterface: '@http_discovery.psr17_factory'
Psr\Http\Message\ServerRequestFactoryInterface: '@http_discovery.psr17_factory'
Psr\Http\Message\StreamFactoryInterface: '@http_discovery.psr17_factory'
Psr\Http\Message\UploadedFileFactoryInterface: '@http_discovery.psr17_factory'
Psr\Http\Message\UriFactoryInterface: '@http_discovery.psr17_factory'
http_discovery.psr17_factory:
class: Http\Discovery\Psr17Factory
+16
View File
@@ -0,0 +1,16 @@
# The website chatbot's MCP server, called by the n8n AI Agent through its MCP Client Tool
# node. Read-only tools over the mirrored TYPO3 content and our own trip data; see
# docs/chatbot-mcp.md. Authentication is the "mcp" firewall in security.yaml.
mcp:
servers:
chatbot:
name: 'MyE&P chatbot'
version: '1.0.0'
instructions: 'Answers questions about the trips and website content of E&P Reisen and its brands. Every result carries a url to cite. Prices and availability are a snapshot as of dataAsOf; the booking page is binding.'
http:
path: /mcp
# DNS rebinding protection guards unauthenticated local servers. This one is
# public, behind a bearer token, and never called from a browser.
allowed_hosts: false
registry:
tools: ['App\Chatbot\Mcp\Tool\']
+15 -2
View File
@@ -5,6 +5,7 @@ monolog:
- core
- auth
- admin
- chatbot
when@dev:
monolog:
@@ -33,11 +34,16 @@ when@dev:
path: "%kernel.logs_dir%/%kernel.environment%.admin.log"
level: debug
channels: ["admin"]
chatbot:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.chatbot.log"
level: debug
channels: ["chatbot"]
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event", "!bpn", "!core", "!auth", "!admin"]
channels: ["!event", "!bpn", "!core", "!auth", "!admin", "!chatbot"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
@@ -96,13 +102,20 @@ when@prod: &prod_config
path: "%kernel.logs_dir%/%kernel.environment%.admin.log"
level: info
channels: ["admin"]
# MCP tool calls, file only: the log for judging search quality later.
chatbot:
type: rotating_file
max_files: 30
path: "%kernel.logs_dir%/%kernel.environment%.chatbot.log"
level: info
channels: ["chatbot"]
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
channels: ["!bpn", "!core", "!auth", "!admin"]
channels: ["!bpn", "!core", "!auth", "!admin", "!chatbot"]
nested:
type: rotating_file
path: "%kernel.logs_dir%/%kernel.environment%.framework.log"
+12
View File
@@ -15,6 +15,11 @@ security:
mailjet:
password: '%env(MAILJET_WEBHOOK_BASIC_PASSWORD)%'
roles: [ ROLE_MAILJET_WEBHOOK ]
mcp_provider:
memory:
users:
n8n:
roles: [ ROLE_CHATBOT_MCP ]
role_hierarchy:
# ROLE_CUSTOMER_EXPERT gates the booking-draft and log surfaces, which used to be
@@ -45,6 +50,12 @@ security:
provider: mailjet_webhook_provider
http_basic:
realm: 'Mailjet Webhook'
mcp:
pattern: ^/mcp$
stateless: true
provider: mcp_provider
access_token:
token_handler: App\Security\McpAccessTokenHandler
main:
lazy: true
provider: app_user_provider
@@ -58,6 +69,7 @@ security:
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/webhooks/mailjet/newsletter$, roles: ROLE_MAILJET_WEBHOOK, requires_channel: https }
- { path: ^/mcp$, roles: ROLE_CHATBOT_MCP, requires_channel: https }
- { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED, requires_channel: https }
- { path: ^/, roles: PUBLIC_ACCESS, requires_channel: https }
+3
View File
@@ -0,0 +1,3 @@
mcp:
resource: .
type: mcp
+9
View File
@@ -333,6 +333,15 @@ services:
- '@App\Dashboard\Widget\PendingRoleApprovalsWidgetProvider'
- '@App\Dashboard\Widget\RecentLogEntriesWidgetProvider'
App\ContentFeed\ContentFeedClient:
arguments:
$baseUrl: '%env(APP_CMS_API_BASE_URL)%'
$apiKey: '%env(APP_CMS_API_KEY)%'
App\Security\McpAccessTokenHandler:
arguments:
$token: '%env(APP_MCP_TOKEN)%'
App\Service\CmsDataProvider:
arguments:
$httpClient: '@typo3.client'