From a7e40b5d8edecf9e9f20bbdbfcd2f43563ce603b Mon Sep 17 00:00:00 2001 From: alexbainter Date: Mon, 20 Jul 2026 14:14:18 -0500 Subject: [PATCH 1/6] Refactor public schema manager and update references --- core/api/views.py | 4 +--- core/forms.py | 4 ++-- core/mcp/server.py | 11 ++--------- core/models.py | 30 +++++++++++++++++++----------- core/views.py | 15 +++++---------- tests/test_search.py | 14 +++++++------- 6 files changed, 36 insertions(+), 42 deletions(-) diff --git a/core/api/views.py b/core/api/views.py index 291b16f..6b2c86e 100644 --- a/core/api/views.py +++ b/core/api/views.py @@ -39,9 +39,7 @@ def docs(request): @require_GET def find(request): id_value = request.GET.get("id") - published_schema_refs = SchemaRef.objects.filter( - schema__in=Schema.public_objects.all() - ) + published_schema_refs = SchemaRef.objects.filter(schema__in=Schema.objects.public()) schema_ref = get_object_or_404(published_schema_refs, id_value__iexact=id_value) return ApiResponse({"url": schema_ref.url}) diff --git a/core/forms.py b/core/forms.py index d2d29ba..f8fb8f3 100644 --- a/core/forms.py +++ b/core/forms.py @@ -83,13 +83,13 @@ def clean_url(self): # If the schema is unpublished, we don't care if the URL or $id are already in use if ( self.schema_id is None - or not Schema.public_objects.filter(id=self.schema_id).exists() + or not Schema.objects.public().filter(id=self.schema_id).exists() ): return url # But if it's a published schema, we need to make sure the URL and $id aren't already in use schema_refs = SchemaRef.objects.select_related("schema").filter( - schema__in=Schema.public_objects.exclude(id=self.schema_id) + schema__in=Schema.objects.public().exclude(id=self.schema_id) ) # First check the URL for schema_ref in schema_refs: diff --git a/core/mcp/server.py b/core/mcp/server.py index 6ecce8a..b7b75c9 100644 --- a/core/mcp/server.py +++ b/core/mcp/server.py @@ -2,8 +2,6 @@ from jsonschema import ValidationError as JSONValidationError from django.core.exceptions import ValidationError as DjangoValidationError from django.urls import reverse -from django.db.models import Q -from django.utils import timezone from mcp.server.fastmcp import FastMCP from core.models import Schema from asgiref.sync import sync_to_async @@ -61,20 +59,15 @@ async def get_schema(schema_id: int): @sync_to_async def fetch_from_db(): - # TODO: dedupe from core.views:lookup_schema - schema_filter = Q(published_at__lte=timezone.now()) - - if user and user.is_authenticated: - schema_filter |= Q(created_by=user) - try: schema = ( Schema.objects + .accessible_to(user) .prefetch_related("schemaref_set") .prefetch_related("documentationitem_set") - .filter(schema_filter) .get(pk=schema_id) ) + return schema.to_manifest() except Schema.DoesNotExist: return None diff --git a/core/models.py b/core/models.py index 346deee..e398479 100644 --- a/core/models.py +++ b/core/models.py @@ -1,6 +1,7 @@ import logging from itertools import chain from django.db import models, transaction +from django.db.models import Q from django.contrib.auth.models import User from django.utils import timezone from django.conf import settings @@ -96,6 +97,10 @@ class Meta: class SchemaQuerySet(models.QuerySet): + def _get_public_q(self): + """Helper method to return the Q object for public schemas.""" + return Q(published_at__isnull=False, published_at__lte=timezone.now()) + def search(self, query_text): """ Rank schemas by full-text relevance against `query_text`. @@ -117,14 +122,18 @@ def search(self, query_text): .order_by("-rank", "name") ) + def public(self): + """Returns only public schemas.""" + return self.filter(self._get_public_q()) -class PublicSchemaManager(models.Manager.from_queryset(SchemaQuerySet)): - def get_queryset(self): - return ( - super() - .get_queryset() - .filter(published_at__isnull=False, published_at__lte=timezone.now()) - ) + def accessible_to(self, user): + """Returns schemas that are either public OR created by the user.""" + q_filter = self._get_public_q() + + if user and user.is_authenticated: + q_filter |= Q(created_by=user) + + return self.filter(q_filter) class PublishedSchemaConflictError(Exception): @@ -142,8 +151,7 @@ def __init__(self, conflicting_schema_ref, reason): class Schema(BaseModel): - objects = models.Manager() - public_objects = PublicSchemaManager() + objects = SchemaQuerySet.as_manager() name = models.CharField(max_length=200) published_at = models.DateTimeField(blank=True, null=True) permanent_urls = GenericRelation(PermanentURL, related_query_name="schema") @@ -265,7 +273,7 @@ def check_for_published_conflicts(self): PublishedSchemaConflictError: If a conflict is found. """ published_schema_refs = SchemaRef.objects.filter( - schema__in=Schema.public_objects.all() + schema__in=Schema.objects.public() ).exclude( # We don't want to check against this Schema's own SchemaRefs schema=self @@ -826,7 +834,7 @@ def __str__(self): @property def public_schemas(self): - return Schema.public_objects.filter( + return Schema.objects.public().filter( created_by_id__in=self.profile_set.values_list("user_id", flat=True) ) diff --git a/core/views.py b/core/views.py index 60106d7..37084c2 100644 --- a/core/views.py +++ b/core/views.py @@ -2,7 +2,6 @@ import uuid from django.shortcuts import render, get_object_or_404, redirect -from django.db.models import Q from django.utils.html import escape from django.utils.safestring import mark_safe from django.contrib.auth.decorators import login_required @@ -78,16 +77,11 @@ def lookup_schema(function): @wraps(function) def _wrap_request(request, schema_id, *args, **kwargs): - schema_filter = Q(published_at__lte=timezone.now()) - - if request.user.is_authenticated: - schema_filter |= Q(created_by=request.user) - schema = get_object_or_404( Schema.objects + .accessible_to(request.user) .prefetch_related("schemaref_set") - .prefetch_related("documentationitem_set") - .filter(schema_filter), + .prefetch_related("documentationitem_set"), pk=schema_id, ) @@ -127,7 +121,8 @@ def render_markdown(markdown_source_text): def index(request): defined_schemas = ( - Schema.public_objects + Schema.objects + .public() .prefetch_related("schemaref_set") .exclude(schemaref__isnull=True) .order_by("name") @@ -452,7 +447,7 @@ def organization_detail(request, organization_id): @login_required def manage_schema_permanent_urls(request, schema_id): schema = get_object_or_404( - Schema.public_objects, + Schema.objects.public(), id=schema_id, created_by=request.user, ) diff --git a/tests/test_search.py b/tests/test_search.py index 92f014e..74dabdd 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -11,7 +11,7 @@ def test_stemming_makes_singular_and_plural_equivalent(): schema = SchemaFactory(name="Invoice Schema") decoy = SchemaFactory(name="Weather Report") for term in ("Schemas", "Schema"): - results = Schema.public_objects.search(term) + results = Schema.objects.public().search(term) assert schema in results assert decoy not in results @@ -19,13 +19,13 @@ def test_stemming_makes_singular_and_plural_equivalent(): @pytest.mark.django_db def test_search_is_case_insensitive(): schema = SchemaFactory(name="Invoice Schema") - assert schema in Schema.public_objects.search("INVOICE") + assert schema in Schema.objects.public().search("INVOICE") @pytest.mark.django_db def test_search_matches_description_field(): schema = SchemaFactory(name="ACME Format", description="A standard for invoices") - assert schema in Schema.public_objects.search("invoice") + assert schema in Schema.objects.public().search("invoice") @pytest.mark.django_db @@ -35,7 +35,7 @@ def test_name_hit_outranks_description_only_hit(): # which would hide a weight bug for some names. SchemaFactory(name="Invoice Format", description=None) SchemaFactory(name="ACME Format", description="Used for invoice data") - ranks = {s.name: s.rank for s in Schema.public_objects.search("invoice")} + ranks = {s.name: s.rank for s in Schema.objects.public().search("invoice")} assert ranks["Invoice Format"] > ranks["ACME Format"] @@ -47,7 +47,7 @@ def test_blank_query_returns_all_and_preserves_order(): # queryset unchanged, so the caller's ordering survives and no relevance # ranking is applied. (Whitespace-only counts as blank via .strip().) for blank in ("", " ", None): - results = Schema.public_objects.order_by("name").search(blank) + results = Schema.objects.public().order_by("name").search(blank) assert [schema.name for schema in results] == ["One", "Two"] @@ -55,7 +55,7 @@ def test_blank_query_returns_all_and_preserves_order(): def test_search_excludes_unpublished_schemas(): SchemaFactory(name="Public Invoice") SchemaFactory(name="Private Invoice", published_at=None) - assert Schema.public_objects.search("invoice").count() == 1 + assert Schema.objects.public().search("invoice").count() == 1 @pytest.mark.django_db @@ -63,7 +63,7 @@ def test_malformed_query_does_not_raise(): SchemaFactory(name="Invoice") # websearch_to_tsquery tolerates junk such as unbalanced quote plus dangling operator. # The guarantee is that evaluating the queryset does not raise. - results = Schema.public_objects.search('"invoice OR -') + results = Schema.objects.public().search('"invoice OR -') assert results.count() >= 0 From 9fdd31fbffb6303731718dfcbd3213fdc73eedb2 Mon Sep 17 00:00:00 2001 From: alexbainter Date: Wed, 22 Jul 2026 12:02:13 -0500 Subject: [PATCH 2/6] Paginate search_schemas results --- core/mcp/server.py | 76 +++++++++++++++++-- tests/test_mcp.py | 181 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 248 insertions(+), 9 deletions(-) diff --git a/core/mcp/server.py b/core/mcp/server.py index b7b75c9..bf1453a 100644 --- a/core/mcp/server.py +++ b/core/mcp/server.py @@ -1,7 +1,10 @@ import json +from typing import Literal from jsonschema import ValidationError as JSONValidationError from django.core.exceptions import ValidationError as DjangoValidationError +from django.db.models import Q from django.urls import reverse +from django.utils import timezone from mcp.server.fastmcp import FastMCP from core.models import Schema from asgiref.sync import sync_to_async @@ -14,12 +17,15 @@ def format_schema(schema): formatted_schema = f""" -Name: {schema.name} ID: {schema.id} +Name: {schema.name} """ if schema.description: - formatted_schema += f"""Description: {schema.description} -""" + formatted_schema += f"Description: {schema.description}\n" + + if schema.published_at is None or schema.published_at > timezone.now(): + formatted_schema += "Visibility: Private\n" + return formatted_schema @@ -34,15 +40,69 @@ def ensure_current_user(): # Note: We don't use type hints elsewhere in the codebase, # but they can influence FastMCP's behavior for tools and resources. +# Function descriptions are the actual descriptions surfaced to models. + +MAX_PAGE_SIZE = 10 -# TODO: This will need to be paginated @mcp.tool() @sync_to_async -def list_schemas(): - """List all available schemas.""" - public_schemas = [format_schema(schema) for schema in Schema.public_objects.all()] - return "\n---\n".join(public_schemas) +def search_schemas( + keywords: list[str] = [], scope: Literal["all", "user"] = "all", page: int = 1 +): + """ + Search for schemas. + + Args: + keywords: A list of search keywords. Keywords are compared to each schema's $id, name, and description for possible matches. Omit to list all schemas in scope. + scope: 'user' to search only the user's own schemas (including private), or 'all' to search the entire registry. Defaults to 'all.' + page: Which page of search results to return. Defaults to 1. + """ + + user = ensure_current_user() + + results = ( + Schema.objects.accessible_to(user) + if scope == "all" + else Schema.objects.filter(created_by=user) + ) + for keyword in keywords: + results = results.filter( + Q(name__icontains=keyword) + | Q(description__icontains=keyword) + | Q(schemaref__id_value__iexact=keyword) + ) + results = results.distinct() + + total_count = results.count() + if total_count == 0: + return "No results matched your query." + + total_pages = (total_count + MAX_PAGE_SIZE - 1) // MAX_PAGE_SIZE + if page < 1 or page > total_pages: + raise ValueError( + f"Invalid page number for query. Please request a page between 1 and {total_pages}." + ) + + start = (page - 1) * MAX_PAGE_SIZE + end = start + MAX_PAGE_SIZE + paginated_results = results[start:end] + + formatted_results = [format_schema(schema) for schema in paginated_results] + formatted_page = "\n---\n".join(formatted_results) + + response = f"Found {results.count()} schemas matching your query{':' if total_pages == 1 else '.'}" + + if total_pages == 1: + response += f"\n\n{formatted_page}" + return response + + response += f"\n\nThe results are truncated. Showing page {page} of {total_pages}:" + response += f"\n\n{formatted_page}" + + response += f'\n\nTo get the next page, use `search_schemas(keywords: , scope: "{scope}", page: {page + 1})' + + return response @mcp.resource("schema://manifest.json") diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 53f6a4f..9ff1493 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -1,11 +1,12 @@ import pytest import json +import requests_mock from mcp.shared.memory import create_connected_server_and_client_session from asgiref.sync import sync_to_async from unittest.mock import patch, MagicMock from starlette.responses import JSONResponse from django.test import override_settings -from core.mcp.server import mcp +from core.mcp.server import mcp, MAX_PAGE_SIZE from core.mcp.api_key_authentication import MCPAPIKeyAuthenticationMiddleware from factories import SchemaFactory, ProfileFactory, UserFactory, SchemaRefFactory from utils import assert_schema_matches_manifest @@ -401,3 +402,181 @@ async def test_update_schema_validation_error(error_client_session, current_user ) assert result.isError assert expected_error_message in result.content[0].text + + +@pytest.mark.anyio +async def test_search_schemas_unauthenticated(error_client_session, current_user_mock): + # Mock the current_user to be None + current_user_mock.get.return_value = None + + expected_error_message = "Not authenticated." + result = await error_client_session.call_tool("search_schemas", arguments={}) + + assert result.isError + assert expected_error_message in result.content[0].text + + +@pytest.mark.anyio +async def test_search_schemas_no_results(client_session, current_user_mock): + user = await sync_to_async(UserFactory.create)() + current_user_mock.get.return_value = user + + result = await client_session.call_tool( + "search_schemas", arguments={"keywords": ["nonexistent_keyword"]} + ) + + assert result.content[0].text == "No results matched your query." + + +@pytest.mark.anyio +async def test_search_schemas_scope(client_session, current_user_mock): + user1 = await sync_to_async(UserFactory.create)() + user2 = await sync_to_async(UserFactory.create)() + current_user_mock.get.return_value = user1 + + # Create a schema for user1 + await sync_to_async(SchemaFactory.create)(created_by=user1, name="User One Schema") + + # Create an accessible (published) schema for user2 + from django.utils import timezone + + await sync_to_async(SchemaFactory.create)( + created_by=user2, name="User Two Public Schema", published_at=timezone.now() + ) + + # Search with 'user' scope - should only return user1's schema + user_scope_result = await client_session.call_tool( + "search_schemas", arguments={"scope": "user"} + ) + assert "User One Schema" in user_scope_result.content[0].text + assert "User Two Public Schema" not in user_scope_result.content[0].text + + # Search with 'all' scope - should return both + all_scope_result = await client_session.call_tool( + "search_schemas", arguments={"scope": "all"} + ) + assert "User One Schema" in all_scope_result.content[0].text + assert "User Two Public Schema" in all_scope_result.content[0].text + + +@pytest.mark.anyio +async def test_search_schemas_description_keyword_filtering( + client_session, current_user_mock +): + user = await sync_to_async(UserFactory.create)() + current_user_mock.get.return_value = user + + await sync_to_async(SchemaFactory.create)( + created_by=user, name="Alpha", description="A special testing schema" + ) + await sync_to_async(SchemaFactory.create)( + created_by=user, name="Beta", description="Another item entirely" + ) + + result = await client_session.call_tool( + "search_schemas", arguments={"keywords": ["SPECIAL"]} + ) + text = result.content[0].text + + assert "Alpha" in text + assert "Beta" not in text + + +@pytest.mark.anyio +async def test_search_schemas_name_keyword_filtering(client_session, current_user_mock): + user = await sync_to_async(UserFactory.create)() + current_user_mock.get.return_value = user + + await sync_to_async(SchemaFactory.create)( + created_by=user, name="Alpha", description="A special testing schema" + ) + await sync_to_async(SchemaFactory.create)( + created_by=user, name="Beta", description="Another item entirely" + ) + + result = await client_session.call_tool( + "search_schemas", arguments={"keywords": ["alpha"]} + ) + text = result.content[0].text + + assert "Alpha" in text + assert "Beta" not in text + + +@pytest.mark.anyio +async def test_search_schemas_id_value_keyword_filtering( + client_session, current_user_mock +): + user = await sync_to_async(UserFactory.create)() + current_user_mock.get.return_value = user + + schema = await sync_to_async(SchemaFactory.create)( + created_by=user, name="Alpha", description="A special testing schema" + ) + mock_url = "https://example.com/schema.json" + mock_id_value = "https://example.com/mockid" + mock_content = f'{{"$id":"{mock_id_value}"}}' + with requests_mock.Mocker() as m: + m.get(mock_url, text=mock_content) + await sync_to_async(SchemaRefFactory.create)(url=mock_url, schema=schema) + + await sync_to_async(SchemaFactory.create)( + created_by=user, name="Beta", description="Another item entirely" + ) + + result = await client_session.call_tool( + "search_schemas", arguments={"keywords": ["special"]} + ) + text = result.content[0].text + + assert "Alpha" in text + assert "Beta" not in text + + +@pytest.mark.anyio +async def test_search_schemas_pagination(client_session, current_user_mock): + user = await sync_to_async(UserFactory.create)() + current_user_mock.get.return_value = user + + # Trigger pagination + for i in range(MAX_PAGE_SIZE + 1): + await sync_to_async(SchemaFactory.create)( + created_by=user, name=f"Pagination Schema {i}" + ) + + # Fetch page 1 + result_page_1 = await client_session.call_tool( + "search_schemas", arguments={"page": 1} + ) + text_1 = result_page_1.content[0].text + + assert f"Found {MAX_PAGE_SIZE + 1} schemas matching your query." in text_1 + assert "The results are truncated. Showing page 1 of 2:" in text_1 + assert "To get the next page, use `search_schemas" in text_1 + + # Fetch page 2 + result_page_2 = await client_session.call_tool( + "search_schemas", arguments={"page": 2} + ) + text_2 = result_page_2.content[0].text + + assert "Showing page 2 of 2:" in text_2 + + +@pytest.mark.anyio +async def test_search_schemas_invalid_page(error_client_session, current_user_mock): + user = await sync_to_async(UserFactory.create)() + current_user_mock.get.return_value = user + + # Create 1 schema so there is only 1 page + await sync_to_async(SchemaFactory.create)(created_by=user) + + result = await error_client_session.call_tool( + "search_schemas", arguments={"page": 5} + ) + + assert result.isError + assert ( + "Invalid page number for query. Please request a page between 1 and 1." + in result.content[0].text + ) From 38c1f432c8056c2dd19fff139d602c7ef2c83633 Mon Sep 17 00:00:00 2001 From: alexbainter Date: Wed, 22 Jul 2026 15:38:05 -0500 Subject: [PATCH 3/6] Update search strategy to use new full text search --- core/mcp/server.py | 22 +++++++++++----------- tests/test_mcp.py | 14 +++++++------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/core/mcp/server.py b/core/mcp/server.py index bf1453a..8e35e42 100644 --- a/core/mcp/server.py +++ b/core/mcp/server.py @@ -2,7 +2,6 @@ from typing import Literal from jsonschema import ValidationError as JSONValidationError from django.core.exceptions import ValidationError as DjangoValidationError -from django.db.models import Q from django.urls import reverse from django.utils import timezone from mcp.server.fastmcp import FastMCP @@ -48,31 +47,32 @@ def ensure_current_user(): @mcp.tool() @sync_to_async def search_schemas( - keywords: list[str] = [], scope: Literal["all", "user"] = "all", page: int = 1 + query: str = None, scope: Literal["all", "user"] = "all", page: int = 1 ): """ Search for schemas. Args: - keywords: A list of search keywords. Keywords are compared to each schema's $id, name, and description for possible matches. Omit to list all schemas in scope. + query: A search query. Can be a list of keywords or an $id. Pass None or an empty string to list all schemas in scope. scope: 'user' to search only the user's own schemas (including private), or 'all' to search the entire registry. Defaults to 'all.' page: Which page of search results to return. Defaults to 1. """ user = ensure_current_user() - results = ( + scope_results = ( Schema.objects.accessible_to(user) if scope == "all" else Schema.objects.filter(created_by=user) ) - for keyword in keywords: - results = results.filter( - Q(name__icontains=keyword) - | Q(description__icontains=keyword) - | Q(schemaref__id_value__iexact=keyword) - ) - results = results.distinct() + + matched_by_id_value = scope_results.filter(schemaref__id_value__iexact=query) + + # If there is a query and it matches an exact ID, skip the full-text search. + if query and matched_by_id_value.exists(): + results = matched_by_id_value + else: + results = scope_results.search(query) total_count = results.count() if total_count == 0: diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 9ff1493..24409df 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -422,7 +422,7 @@ async def test_search_schemas_no_results(client_session, current_user_mock): current_user_mock.get.return_value = user result = await client_session.call_tool( - "search_schemas", arguments={"keywords": ["nonexistent_keyword"]} + "search_schemas", arguments={"query": "nonexistent"} ) assert result.content[0].text == "No results matched your query." @@ -460,7 +460,7 @@ async def test_search_schemas_scope(client_session, current_user_mock): @pytest.mark.anyio -async def test_search_schemas_description_keyword_filtering( +async def test_search_schemas_description_query_filtering( client_session, current_user_mock ): user = await sync_to_async(UserFactory.create)() @@ -474,7 +474,7 @@ async def test_search_schemas_description_keyword_filtering( ) result = await client_session.call_tool( - "search_schemas", arguments={"keywords": ["SPECIAL"]} + "search_schemas", arguments={"query": "SPECIAL"} ) text = result.content[0].text @@ -483,7 +483,7 @@ async def test_search_schemas_description_keyword_filtering( @pytest.mark.anyio -async def test_search_schemas_name_keyword_filtering(client_session, current_user_mock): +async def test_search_schemas_name_query_filtering(client_session, current_user_mock): user = await sync_to_async(UserFactory.create)() current_user_mock.get.return_value = user @@ -495,7 +495,7 @@ async def test_search_schemas_name_keyword_filtering(client_session, current_use ) result = await client_session.call_tool( - "search_schemas", arguments={"keywords": ["alpha"]} + "search_schemas", arguments={"query": "alpha"} ) text = result.content[0].text @@ -504,7 +504,7 @@ async def test_search_schemas_name_keyword_filtering(client_session, current_use @pytest.mark.anyio -async def test_search_schemas_id_value_keyword_filtering( +async def test_search_schemas_id_value_query_filtering( client_session, current_user_mock ): user = await sync_to_async(UserFactory.create)() @@ -525,7 +525,7 @@ async def test_search_schemas_id_value_keyword_filtering( ) result = await client_session.call_tool( - "search_schemas", arguments={"keywords": ["special"]} + "search_schemas", arguments={"query": mock_id_value.upper()} ) text = result.content[0].text From 5ab288c10c7528a2d13d968790c9110929df382a Mon Sep 17 00:00:00 2001 From: alexbainter Date: Mon, 27 Jul 2026 11:27:57 -0500 Subject: [PATCH 4/6] Fix pluralization of single result queries --- core/mcp/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/mcp/server.py b/core/mcp/server.py index 8e35e42..8656b7f 100644 --- a/core/mcp/server.py +++ b/core/mcp/server.py @@ -91,7 +91,7 @@ def search_schemas( formatted_results = [format_schema(schema) for schema in paginated_results] formatted_page = "\n---\n".join(formatted_results) - response = f"Found {results.count()} schemas matching your query{':' if total_pages == 1 else '.'}" + response = f"Found {results.count()} schema{'s' if results.count() > 1 else ''} matching your query{':' if total_pages == 1 else '.'}" if total_pages == 1: response += f"\n\n{formatted_page}" From 9c4c469822c1abaee4feef8b0e50c1a32b12fd6c Mon Sep 17 00:00:00 2001 From: alexbainter Date: Mon, 27 Jul 2026 11:46:38 -0500 Subject: [PATCH 5/6] Pin ruff to 0.15.* --- .github/workflows/ci-cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 6e5b44f..51342a3 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -41,7 +41,7 @@ jobs: - name: Install Ruff run: | python -m pip install --upgrade pip - pip install ruff + pip install "ruff>=0.15,<0.16" - name: Lint run: ruff check --output-format github @@ -57,7 +57,7 @@ jobs: - name: Install Ruff run: | python -m pip install --upgrade pip - pip install ruff + pip install "ruff>=0.15,<0.16" - name: Lint # --output-format github requires --preview From 68d0246901646a9ba0ec9ba99453a65b42d4a845 Mon Sep 17 00:00:00 2001 From: alexbainter Date: Mon, 27 Jul 2026 14:26:20 -0500 Subject: [PATCH 6/6] Two quick fixes --- core/mcp/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mcp/server.py b/core/mcp/server.py index 8656b7f..b354409 100644 --- a/core/mcp/server.py +++ b/core/mcp/server.py @@ -91,7 +91,7 @@ def search_schemas( formatted_results = [format_schema(schema) for schema in paginated_results] formatted_page = "\n---\n".join(formatted_results) - response = f"Found {results.count()} schema{'s' if results.count() > 1 else ''} matching your query{':' if total_pages == 1 else '.'}" + response = f"Found {total_count} schema{'s' if total_count > 1 else ''} matching your query{':' if total_pages == 1 else '.'}" if total_pages == 1: response += f"\n\n{formatted_page}" @@ -100,7 +100,7 @@ def search_schemas( response += f"\n\nThe results are truncated. Showing page {page} of {total_pages}:" response += f"\n\n{formatted_page}" - response += f'\n\nTo get the next page, use `search_schemas(keywords: , scope: "{scope}", page: {page + 1})' + response += f'\n\nTo get the next page, use `search_schemas(query: , scope: "{scope}", page: {page + 1})' return response