Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
75a9652
Add low-level MCP contrib package with tools and tests
nilbacardit26 Feb 27, 2026
54e355e
Use catalog-first lookup in MCP list_children tool
nilbacardit26 Feb 27, 2026
a3d2f5b
Added pagination to mcp list children.
finalchaz Mar 4, 2026
4cfac03
Merge branch 'feat/mcp-lowlevel-contrib' of github.com:plone/guilloti…
finalchaz Mar 4, 2026
5693ff4
- Seach inside catalog
finalchaz Mar 4, 2026
ec97b13
Added mcp/resources (also solved isort, black and flake8)
finalchaz Mar 5, 2026
6a4ef4e
limit mcp to python >= 3.10
finalchaz Mar 6, 2026
d73f294
Changes to help mcp discovery and use.
finalchaz Mar 6, 2026
370c18c
flake
finalchaz Mar 6, 2026
55ce946
- Code cleaning, improved tool discovery
finalchaz Mar 9, 2026
9cbbb81
- check if mcp sdk is missing
finalchaz Mar 9, 2026
0afec7e
chore: black format
finalchaz Mar 9, 2026
7571b29
chore: test correction
finalchaz Mar 9, 2026
881a7e0
optional invalidate cache using redis
nilbacardit26 Mar 25, 2026
202bf80
adding logs
nilbacardit26 Mar 25, 2026
ce2ad9d
upgrade pytest
nilbacardit26 Mar 25, 2026
4c2f748
passing tests
nilbacardit26 Mar 30, 2026
f3fd153
isort
nilbacardit26 Mar 30, 2026
b7bb67f
passing tests, fixing memcached test
nilbacardit26 Mar 30, 2026
fc66dee
upgradung uvicorn, disabling server_headers, let guill do it
nilbacardit26 Mar 30, 2026
ab88f58
dropping test support for 3.8 and 3.9, adding 3.12
nilbacardit26 Mar 30, 2026
40674c0
passing tests
nilbacardit26 Mar 30, 2026
33b0943
passing tests
nilbacardit26 Mar 30, 2026
4efc9fa
new isort and black checkers
nilbacardit26 Mar 30, 2026
6b007f0
flake8 checkers
nilbacardit26 Mar 30, 2026
d95d322
isort reformatting
nilbacardit26 Mar 30, 2026
63900e5
using jinja2 FileSystemloader and upgrade test suite semantics
nilbacardit26 Mar 30, 2026
fd05354
flake8 isort
nilbacardit26 Mar 30, 2026
869d407
keep existing invalidation going on mcp, ignore new ones
nilbacardit26 Mar 30, 2026
ef98951
black
nilbacardit26 Mar 30, 2026
f3c2c7e
mypy
nilbacardit26 Mar 30, 2026
9c258bf
passing tests
nilbacardit26 Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 3 additions & 3 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, '3.10', '3.11']
python-version: ['3.10', '3.11', '3.12']

steps:
- name: Checkout the repository
Expand All @@ -26,15 +26,15 @@ jobs:
- name: Run pre-checks
run: |
flake8 guillotina --config=setup.cfg
isort -c -rc guillotina/
isort --check-only guillotina/
black --check --verbose guillotina
# Job to run tests
tests:
runs-on: ubuntu-latest

strategy:
matrix:
python-version: [3.8, 3.9, '3.10', '3.11']
python-version: ['3.10', '3.11', '3.12']
database: ["DUMMY", "postgres", "cockroachdb"]
db_schema: ["custom", "public"]
exclude:
Expand Down
8 changes: 4 additions & 4 deletions .isort.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[settings]
force_single_line=True
sections=THIRDPARTY,FIRSTPARTY,LOCALFOLDER,STDLIB
no_lines_before=LOCALFOLDER,THIRDPARTY,FIRSTPARTY,STDLIB
force_alphabetical_sort=True
profile=black
lines_after_imports=2
line_length=110
skip_glob=*.pyi
41 changes: 41 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# AGENTS.md

## Project Overview
- Purpose: Guillotina core framework and official contrib addons.
- Main stack: Python async API server (ASGI), PostgreSQL, optional Redis.
- Key paths:
- `guillotina/` core framework and contrib packages
- `guillotina/tests/` test suite
- `docs/source/` documentation

## Development Commands
- Setup (local venv expected at repo root):
- `python3 -m venv .venv`
- `source .venv/bin/activate`
- `pip install -r requirements.txt`
- `pip install -r contrib-requirements.txt`
- `pip install -e '.[test]'`
- Run local server:
- `g` (uses `config.yaml` by default)
- Run tests:
- `.venv/bin/pytest guillotina/tests`
- Targeted: `.venv/bin/pytest guillotina/tests/<path>`

## Validation
- For contrib changes, run focused tests under the touched contrib test folder.
- For API/service changes, verify status codes and response payload contracts.
- Keep docs updated under `docs/source/contrib/` when adding contrib features.

## Deployment Notes
- This repo is a framework/library; no direct client deployment from this repo by default.
- Build/release lifecycle should follow package versioning (`VERSION`, `CHANGELOG.rst`).

## Constraints / Gotchas
- Keep compatibility with repository formatting (`black` line length 110).
- Avoid wrapper layers when task explicitly requires low-level protocol primitives.
- Never commit credentials or local environment files.

## Task Closeout Notes
- Update `CHANGELOG.rst` for notable changes.
- Record branch name, commit hash, validation output, and task evidence in Ops Tracker.

11 changes: 11 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ CHANGELOG
7.0.7 (unreleased)
------------------

- REAKING CHANGE: Drop support for Python 3.8 and 3.9. Guillotina is
now tested and supported on Python 3.10, 3.11, and 3.12.
[nilbacardit26]
- Add `guillotina.contrib.mcp` with low-level MCP server integration
(`mcp.server.lowlevel`), tool registry utility, MCP services,
cache invalidation subscribers, and tests/docs coverage.
- Optimize MCP `list_children` tool to prefer catalog queries and
fallback to `async_items` when catalog is unavailable.
- Upgrade the pytest stack so the CI test environment stays compatible
with the optional MCP SDK and its AnyIO pytest plugin on Python 3.10+.
[finalchaz, nilbacardit26]
- Docs: Update documentation and configuration settings
- Chore: Update sphinx-guillotina-theme version to 1.0.9
[rboixaderg]
Expand Down
1 change: 1 addition & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest_plugins = ["guillotina.tests.fixtures", "pytest_docker_fixtures"]
7 changes: 4 additions & 3 deletions contrib-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ html2text==2019.8.11
aiosmtplib<4.0.0; python_version <= "3.8"
aiosmtplib>=4.0.0; python_version >= "3.9"
pre-commit==1.18.2
flake8==5.0.4
flake8==6.1.0
codecov==2.1.13
mypy-zope==1.0.11
black==22.3.0
isort==4.3.21
black==24.10.0
isort==5.13.2
jinja2==2.11.3
MarkupSafe<2.1.0
pytz==2020.1
emcache==0.6.0; python_version < '3.10'
pymemcache==3.4.0; python_version < '3.10'
mcp>=1.0.0; python_version >= '3.10'

# Conditional Pillow versions
pillow==10.4.0; python_version < '3.11'
Expand Down
1 change: 1 addition & 0 deletions docs/source/contrib/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ Contents:
swagger
mailer
dbusers
mcp
44 changes: 44 additions & 0 deletions docs/source/contrib/mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# MCP

`guillotina.contrib.mcp` provides a low-level MCP integration layer built on
the official `mcp.server.lowlevel` primitives (no FastMCP wrapper).

## Installation

```bash
pip install "guillotina[mcp]"
```

## Configuration

```yaml
applications:
- guillotina
- guillotina.contrib.mcp

mcp:
enabled: true
server_name: guillotina-mcp
default_child_limit: 50
```

## Runtime endpoints

- `GET /@mcp`: registry metadata and registered tools.
- `GET /@mcp/tools`: tool list and schemas.
- `POST /@mcp/tools/invoke`: executes one tool with payload
`{ "tool": "<name>", "arguments": { ... } }`.
- `GET /@mcp/server/status`: validates low-level SDK availability.

## Built-in tools

- `resolve_path`: resolve a path and return basic metadata.
- `list_children`: list child resources from a folder-like resource.
- `serialize_resource`: execute Guillotina serialization adapters.
- `notify_modified`: emit an `ObjectModifiedEvent`.

The tool registry is implemented as a Guillotina utility and cache invalidation
is handled by subscribers on object add/modify/remove events.

`list_children` prefers catalog-backed lookup when a catalog utility is
available and falls back to `async_items()` iteration when it is not.
20 changes: 15 additions & 5 deletions guillotina/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
# load the patch before anything else.
import os
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path

from zope.interface import Interface # noqa

from guillotina import glogging
from guillotina._cache import BEHAVIOR_CACHE # noqa
from guillotina._cache import FACTORY_CACHE # noqa
from guillotina._cache import PERMISSIONS_CACHE # noqa
from guillotina._cache import SCHEMA_CACHE # noqa
from guillotina._settings import app_settings # noqa
from guillotina.i18n import default_message_factory as _ # noqa
from zope.interface import Interface # noqa

import os
import pkg_resources

def _resolve_version():
try:
return version("guillotina")
except PackageNotFoundError:
return (Path(__file__).resolve().parents[1] / "VERSION").read_text().strip()


__version__ = pkg_resources.get_distribution("guillotina").version
__version__ = _resolve_version()


# create logging
Expand All @@ -21,9 +30,10 @@

if os.environ.get("GDEBUG", "").lower() in ("true", "t", "1"): # pragma: no cover
# patches for extra debugging....
import asyncpg
import time

import asyncpg

original_execute = asyncpg.connection.Connection._do_execute
logger.warning("RUNNING IN DEBUG MODE")

Expand Down
9 changes: 4 additions & 5 deletions guillotina/_settings.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from guillotina import interfaces
from guillotina.db.uid import generate_uid
from typing import Any
from typing import Dict

import copy
import pickle
import string
from typing import Any, Dict

from guillotina import interfaces
from guillotina.db.uid import generate_uid


app_settings: Dict[str, Any] = {
Expand Down
9 changes: 4 additions & 5 deletions guillotina/addons.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from zope.interface import implementer

from guillotina import task_vars
from guillotina._settings import app_settings
from guillotina.interfaces import IAddOn
from guillotina.interfaces import IAddons
from guillotina.utils import apply_coroutine
from guillotina.utils import get_current_request
from zope.interface import implementer
from guillotina.interfaces import IAddOn, IAddons
from guillotina.utils import apply_coroutine, get_current_request


@implementer(IAddOn)
Expand Down
12 changes: 5 additions & 7 deletions guillotina/annotations.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import logging
from collections import UserDict

from zope.interface import implementer

from guillotina import configure
from guillotina.db.interfaces import ITransaction
from guillotina.db.orm.base import BaseObject
from guillotina.exceptions import TransactionNotFound
from guillotina.interfaces import IAnnotationData
from guillotina.interfaces import IAnnotations
from guillotina.interfaces import IRegistry
from guillotina.interfaces import IResource
from guillotina.interfaces import IAnnotationData, IAnnotations, IRegistry, IResource
from guillotina.transactions import get_transaction
from zope.interface import implementer

import logging


logger = logging.getLogger("guillotina")
Expand Down
3 changes: 2 additions & 1 deletion guillotina/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# these imports are done to force loading services
from guillotina.json import definitions # noqa

from . import addons # noqa
from . import aggregation # noqa
from . import app # noqa
Expand All @@ -16,4 +18,3 @@
from . import user # noqa
from . import vocabularies # noqa
from . import ws # noqa
from guillotina.json import definitions # noqa
7 changes: 2 additions & 5 deletions guillotina/api/addons.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
from guillotina import addons
from guillotina import configure
from guillotina import error_reasons
from guillotina import addons, configure, error_reasons
from guillotina._settings import app_settings
from guillotina.i18n import MessageFactory
from guillotina.interfaces import IAddons
from guillotina.interfaces import IContainer
from guillotina.interfaces import IAddons, IContainer
from guillotina.response import ErrorResponse
from guillotina.utils import get_registry

Expand Down
4 changes: 2 additions & 2 deletions guillotina/api/aggregation.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from collections import Counter

from guillotina import configure
from guillotina.component import query_utility
from guillotina.interfaces import ICatalogUtility
from guillotina.interfaces import IResource
from guillotina.interfaces import ICatalogUtility, IResource
from guillotina.response import HTTPServiceUnavailable


Expand Down
6 changes: 2 additions & 4 deletions guillotina/api/app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from guillotina import component
from guillotina import configure
from guillotina import component, configure
from guillotina._settings import app_settings
from guillotina.component import get_multi_adapter
from guillotina.interfaces import IApplication
from guillotina.interfaces import IResourceSerializeToJson
from guillotina.interfaces import IApplication, IResourceSerializeToJson
from guillotina.utils import get_dotted_name


Expand Down
10 changes: 3 additions & 7 deletions guillotina/api/behaviors.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from guillotina import configure
from guillotina.component import get_multi_adapter
from guillotina.component import get_utilities_for
from guillotina.component import query_adapter
from guillotina.component import get_multi_adapter, get_utilities_for, query_adapter
from guillotina.content import get_cached_factory
from guillotina.interfaces import IBehavior
from guillotina.interfaces import IResource
from guillotina.interfaces import ISchemaSerializeToJson
from guillotina.interfaces import IBehavior, IResource, ISchemaSerializeToJson
from guillotina.response import Response
from guillotina.utils import resolve_dotted_name

Expand Down Expand Up @@ -50,7 +46,7 @@ async def default_patch(context, request):
permission="guillotina.ModifyContent",
name="@behaviors/{behavior}",
summary="Remove behavior from resource",
parameters=[{"in": "path", "name": "key", "required": True, "schema": {"type": "string"}}],
parameters=[{"in": "path", "name": "behavior", "required": True, "schema": {"type": "string"}}],
requestBody={
"required": True,
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Behavior"}}},
Expand Down
35 changes: 14 additions & 21 deletions guillotina/api/container.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
from guillotina import addons
from guillotina import app_settings
from guillotina import configure
from guillotina import error_reasons
from guillotina import task_vars
import posixpath
from typing import Optional

from guillotina import addons, app_settings, configure, error_reasons, task_vars
from guillotina.api import content
from guillotina.api.service import Service
from guillotina.component import get_adapter
from guillotina.component import get_multi_adapter
from guillotina.component import get_adapter, get_multi_adapter
from guillotina.content import create_content
from guillotina.event import notify
from guillotina.events import ObjectAddedEvent
from guillotina.interfaces import IAnnotations
from guillotina.interfaces import IApplication
from guillotina.interfaces import IContainer
from guillotina.interfaces import IDatabase
from guillotina.interfaces import IPrincipalRoleManager
from guillotina.interfaces import IResourceSerializeToJson
from guillotina.interfaces import (
IAnnotations,
IApplication,
IContainer,
IDatabase,
IPrincipalRoleManager,
IResourceSerializeToJson,
)
from guillotina.interfaces.content import IGetOwner
from guillotina.registry import REGISTRY_DATA_KEY
from guillotina.response import ErrorResponse
from guillotina.response import HTTPConflict
from guillotina.response import HTTPNotFound
from guillotina.response import HTTPNotImplemented
from guillotina.response import Response
from guillotina.response import ErrorResponse, HTTPConflict, HTTPNotFound, HTTPNotImplemented, Response
from guillotina.utils import get_authenticated_user_id
from typing import Optional

import posixpath


@configure.service(
Expand Down
Loading
Loading