This is an automated email from the ASF dual-hosted git repository.

FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git


The following commit(s) were added to refs/heads/master by this push:
     new ae477a3  docs: define subscription capability boundary (#142)
ae477a3 is described below

commit ae477a3aeb6763e9b15baab9e17798b799b42cf8
Author: Yijia Su <[email protected]>
AuthorDate: Thu Jul 30 10:17:38 2026 +0800

    docs: define subscription capability boundary (#142)
---
 CHANGELOG.md                                       |   3 +
 README.md                                          |  15 +++
 .../0001-subscriptions-require-change-events.md    |  87 +++++++++++++++++
 test/integration/test_real_doris_transports.py     |  38 +++++++-
 test/protocol/test_mcp_v2_protocol.py              | 107 +++++++++++++++++++++
 5 files changed, 249 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e363112..d43ae69 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -49,6 +49,9 @@ under **Unreleased** until a new version is selected and 
published.
   `serverInfo`, and health endpoints on one product version source.
 - Documented the modern request headers, migration steps, and deployment
   constraints.
+- Recorded `subscriptions/listen` as intentionally unavailable until a real
+  cross-worker Doris change-event source exists, with discovery kept
+  capability-honest.
 - Positioned Dynamic Client Registration as a compatibility fallback behind
   preconfigured clients and Client ID Metadata Documents.
 - Persisted static bearer tokens as self-describing SHA-256/SHA-512 digests,
diff --git a/README.md b/README.md
index fc1a68a..3d82c4c 100644
--- a/README.md
+++ b/README.md
@@ -560,6 +560,21 @@ changes, or under another principal returns `Invalid 
Params`. Restart the
 listing without a cursor in that case. This prevents a multi-page traversal
 from silently duplicating, dropping, or crossing permission-scoped entries.
 
+### Subscriptions and Change Notifications
+
+The server does not currently advertise or serve `subscriptions/listen`.
+Tools and prompts have no runtime mutation channel, and Doris catalog metadata
+does not provide this process with a reliable cross-worker change-event source.
+Cache expiry and periodic catalog polling are not treated as change events.
+
+Consequently, MCP 2026-07-28 discovery reports `listChanged: false` for tools,
+prompts, and resources and `subscribe: false` for resources. A
+`subscriptions/listen` request returns `Method not found`; clients should
+refresh lists explicitly. Streamable HTTP and true subprocess stdio tests
+enforce this boundary. See
+[the subscription decision 
record](docs/decisions/0001-subscriptions-require-change-events.md)
+for the conditions required before the capability can be enabled.
+
 ### Migrating from MCP 2025-11-25
 
 1. Upgrade the client to a `2026-07-28`-capable MCP SDK.
diff --git a/docs/decisions/0001-subscriptions-require-change-events.md 
b/docs/decisions/0001-subscriptions-require-change-events.md
new file mode 100644
index 0000000..14e774c
--- /dev/null
+++ b/docs/decisions/0001-subscriptions-require-change-events.md
@@ -0,0 +1,87 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Subscriptions require a real change-event source
+
+- Status: Accepted
+- Date: 2026-07-30
+- Protocol: MCP 2026-07-28
+
+## Context
+
+MCP 2026-07-28 delivers server change notifications through the long-lived
+`subscriptions/listen` request. Registering that request handler also causes
+the SDK to advertise `listChanged` for tools, prompts, and resources, plus
+resource subscription support.
+
+Doris MCP Server currently has no event source that can uphold those
+capabilities:
+
+- tools and prompts are assembled when the process starts and have no runtime
+  mutation channel;
+- Doris resources are read from catalog metadata on demand;
+- the bounded metadata cache has time-based expiry, but expiry is not evidence
+  that a catalog object changed;
+- Doris DDL can be performed outside this server, so observing only tool calls
+  made through this process would miss changes;
+- multiple workers and replicas have no shared catalog-event bus.
+
+Polling a full catalog and treating cache expiry as a change event would create
+false positives, detection gaps, identity-scoping ambiguity, and inconsistent
+behavior across replicas. A handler that never publishes events would be worse:
+discovery would promise a capability that cannot work.
+
+## Decision
+
+Doris MCP Server does not register `subscriptions/listen` until it has a
+reliable change-event source.
+
+For MCP 2026-07-28:
+
+- discovery reports `listChanged: false` for tools, prompts, and resources;
+- discovery reports `subscribe: false` for resources;
+- `subscriptions/listen` returns the standard `Method not found` error;
+- Streamable HTTP and a true subprocess stdio test enforce this boundary and
+  verify that a rejected listen request does not affect later requests.
+
+The older `resources/subscribe` request is not used as a substitute on the
+modern protocol.
+
+## Revisit criteria
+
+Subscription support may be added when all of the following exist:
+
+1. a change source that detects Doris catalog changes regardless of whether
+   they originated through this server;
+2. identity-aware resource filtering that cannot reveal a URI after the
+   principal loses access;
+3. shared fan-out for every configured worker and replica;
+4. bounded buffering, disconnect cleanup, and explicit no-replay behavior;
+5. Streamable HTTP, subprocess stdio, multiworker, authorization, and failure
+   recovery tests against the real event source.
+
+At that point the SDK subscription bus and listen handler can carry typed
+events. Capability flags must be enabled only for event kinds the server can
+actually publish.
+
+## Consequences
+
+Clients must refresh lists explicitly. They can rely on discovery rather than
+opening a stream that will remain silent. The server avoids background catalog
+polling and does not claim unsupported real-time behavior.
diff --git a/test/integration/test_real_doris_transports.py 
b/test/integration/test_real_doris_transports.py
index 0a76f1c..0db2d2b 100644
--- a/test/integration/test_real_doris_transports.py
+++ b/test/integration/test_real_doris_transports.py
@@ -47,8 +47,14 @@ from typing import Any
 import httpx
 import pymysql
 import pytest
-from mcp import Client, StdioServerParameters
+from mcp import Client, MCPError, StdioServerParameters
 from mcp.client.stdio import stdio_client
+from mcp.types import (
+    SubscriptionFilter,
+    SubscriptionsListenRequest,
+    SubscriptionsListenRequestParams,
+    SubscriptionsListenResult,
+)
 
 from doris_mcp_server import __version__
 
@@ -444,6 +450,36 @@ async def 
test_real_doris_protocol_lists_paginate_without_loss(
         environment,
         read_timeout_seconds=60,
     ) as client:
+        capabilities = client.server_capabilities
+        assert capabilities is not None
+        assert capabilities.tools is not None
+        assert capabilities.tools.list_changed is False
+        assert capabilities.prompts is not None
+        assert capabilities.prompts.list_changed is False
+        assert capabilities.resources is not None
+        assert capabilities.resources.list_changed is False
+        assert capabilities.resources.subscribe is False
+
+        with pytest.raises(MCPError) as unsupported:
+            await client.session.send_request(
+                SubscriptionsListenRequest(
+                    params=SubscriptionsListenRequestParams(
+                        notifications=SubscriptionFilter(
+                            tools_list_changed=True,
+                            prompts_list_changed=True,
+                            resources_list_changed=True,
+                            resource_subscriptions=[
+                                f"doris://table/{doris_sandbox.table}"
+                            ],
+                        )
+                    )
+                ),
+                SubscriptionsListenResult,
+                request_read_timeout_seconds=5,
+            )
+        assert unsupported.value.code == -32601
+        assert unsupported.value.message == "Method not found"
+
         for list_method, result_field, identifier in (
             (
                 client.list_resources,
diff --git a/test/protocol/test_mcp_v2_protocol.py 
b/test/protocol/test_mcp_v2_protocol.py
index 30ee3f3..e23212c 100644
--- a/test/protocol/test_mcp_v2_protocol.py
+++ b/test/protocol/test_mcp_v2_protocol.py
@@ -32,6 +32,10 @@ from mcp.types import (
     PromptMessage,
     Resource,
     SamplingCapability,
+    SubscriptionFilter,
+    SubscriptionsListenRequest,
+    SubscriptionsListenRequestParams,
+    SubscriptionsListenResult,
     TextContent,
     Tool,
 )
@@ -434,6 +438,67 @@ async def 
test_http_discover_is_stateless_and_unknown_method_does_not_kill_serve
         assert second.json()["result"]["resultType"] == "complete"
 
 
[email protected]
+async def 
test_http_does_not_advertise_or_serve_subscriptions_without_change_source():
+    server = create_test_server()
+    assert server.get_request_handler("subscriptions/listen") is None
+    app = server.streamable_http_app(
+        json_response=True,
+        stateless_http=True,
+        host="127.0.0.1",
+        transport_security=create_transport_security("127.0.0.1"),
+    )
+
+    async with (
+        app.router.lifespan_context(app),
+        httpx2.ASGITransport(app) as transport,
+        httpx2.AsyncClient(
+            transport=transport,
+            base_url="http://127.0.0.1:3000";,
+        ) as client,
+    ):
+        discovered = await client.post(
+            "/mcp",
+            json=modern_request(1, "server/discover"),
+            headers=modern_headers("server/discover"),
+        )
+        assert discovered.status_code == 200
+        capabilities = discovered.json()["result"]["capabilities"]
+        assert capabilities["tools"]["listChanged"] is False
+        assert capabilities["prompts"]["listChanged"] is False
+        assert capabilities["resources"] == {
+            "subscribe": False,
+            "listChanged": False,
+        }
+
+        listen_request = modern_request(2, "subscriptions/listen")
+        listen_request["params"]["notifications"] = {
+            "toolsListChanged": True,
+            "promptsListChanged": True,
+            "resourcesListChanged": True,
+            "resourceSubscriptions": ["doris://table/orders"],
+        }
+        rejected = await client.post(
+            "/mcp",
+            json=listen_request,
+            headers=modern_headers("subscriptions/listen"),
+        )
+        assert rejected.status_code == 404
+        assert rejected.json()["error"] == {
+            "code": -32601,
+            "message": "Method not found",
+            "data": "subscriptions/listen",
+        }
+
+        recovered = await client.post(
+            "/mcp",
+            json=modern_request(3, "server/discover"),
+            headers=modern_headers("server/discover"),
+        )
+        assert recovered.status_code == 200
+        assert recovered.json()["result"]["resultType"] == "complete"
+
+
 @pytest.mark.asyncio
 async def test_http_rejects_untrusted_origin_and_legacy_adapter_is_stateless():
     app = DorisMCPHTTPTransport(
@@ -1039,6 +1104,48 @@ async def 
test_stdio_validates_capabilities_versions_and_process_survival():
         )
 
 
[email protected]
+async def 
test_true_subprocess_stdio_does_not_advertise_or_serve_subscriptions():
+    server_script = Path(__file__).with_name("stdio_capability_server.py")
+    server_params = StdioServerParameters(
+        command=sys.executable,
+        args=[str(server_script)],
+    )
+
+    async with Client(stdio_client(server_params)) as modern:
+        capabilities = modern.server_capabilities
+        assert capabilities is not None
+        assert capabilities.tools is not None
+        assert capabilities.tools.list_changed is False
+        assert capabilities.prompts is not None
+        assert capabilities.prompts.list_changed is False
+        assert capabilities.resources is not None
+        assert capabilities.resources.list_changed is False
+        assert capabilities.resources.subscribe is False
+
+        listen_request = SubscriptionsListenRequest(
+            params=SubscriptionsListenRequestParams(
+                notifications=SubscriptionFilter(
+                    tools_list_changed=True,
+                    prompts_list_changed=True,
+                    resources_list_changed=True,
+                    resource_subscriptions=["doris://table/orders"],
+                )
+            )
+        )
+        with pytest.raises(MCPError) as unsupported:
+            await modern.session.send_request(
+                listen_request,
+                SubscriptionsListenResult,
+                request_read_timeout_seconds=1,
+            )
+        assert unsupported.value.code == -32601
+        assert unsupported.value.message == "Method not found"
+
+        recovered = await modern.list_resources(cache_mode="bypass")
+        assert recovered.result_type == "complete"
+
+
 @pytest.mark.asyncio
 async def test_stdio_enforces_tool_specific_client_capabilities_and_recovers():
     server_script = Path(__file__).with_name("conformance_server.py")


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to