PyStreamMCP 3.2.0__tar.gz → 3.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {pystreammcp-3.2.0/python/PyStreamMCP.egg-info → pystreammcp-3.3.0}/PKG-INFO +76 -2
  2. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/README.md +75 -1
  3. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/pyproject.toml +1 -1
  4. {pystreammcp-3.2.0 → pystreammcp-3.3.0/python/PyStreamMCP.egg-info}/PKG-INFO +76 -2
  5. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/PyStreamMCP.egg-info/SOURCES.txt +3 -0
  6. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/__init__.py +4 -2
  7. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/api.py +47 -4
  8. pystreammcp-3.3.0/python/pystreammcp/discovery.py +216 -0
  9. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/langchain.py +15 -11
  10. pystreammcp-3.3.0/python/pystreammcp/mcp_server.py +330 -0
  11. pystreammcp-3.3.0/python/pystreammcp/testing.py +123 -0
  12. pystreammcp-3.3.0/tests/test_discovery.py +118 -0
  13. pystreammcp-3.3.0/tests/test_mcp_server_resilience.py +172 -0
  14. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_sprint1_foundation.py +3 -2
  15. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_sprint2_langchain.py +12 -1
  16. pystreammcp-3.2.0/python/pystreammcp/discovery.py +0 -82
  17. pystreammcp-3.2.0/python/pystreammcp/mcp_server.py +0 -283
  18. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/LICENSE +0 -0
  19. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/MANIFEST.in +0 -0
  20. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/PyStreamMCP.egg-info/dependency_links.txt +0 -0
  21. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/PyStreamMCP.egg-info/requires.txt +0 -0
  22. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/PyStreamMCP.egg-info/top_level.txt +0 -0
  23. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/_mcp_connector.py +0 -0
  24. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/_mcp_tools.py +0 -0
  25. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/adapters.py +0 -0
  26. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/agent.py +0 -0
  27. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/cli.py +0 -0
  28. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/cli_daemon.py +0 -0
  29. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/cli_dashboard.py +0 -0
  30. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/context.py +0 -0
  31. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/decomposition.py +0 -0
  32. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/__init__.py +0 -0
  33. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/crewai.py +0 -0
  34. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/haystack.py +0 -0
  35. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/langsmith.py +0 -0
  36. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/llamaindex.py +0 -0
  37. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/pydantic_ai.py +0 -0
  38. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/integrations/semantic_kernel.py +0 -0
  39. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/ml/__init__.py +0 -0
  40. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/ml/relevance.py +0 -0
  41. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/ml/tagging.py +0 -0
  42. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/ml.py +0 -0
  43. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/multi_agent.py +0 -0
  44. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/observability/__init__.py +0 -0
  45. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/observability/metrics.py +0 -0
  46. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/okf_core.py +0 -0
  47. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/okf_discovery.py +0 -0
  48. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/okf_query_planner.py +0 -0
  49. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/optimization/__init__.py +0 -0
  50. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/optimization/advanced.py +0 -0
  51. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/optimization/decomposition.py +0 -0
  52. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/optimization.py +0 -0
  53. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/orchestration/__init__.py +0 -0
  54. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/orchestration/airflow.py +0 -0
  55. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/orchestration/nocode_rpa.py +0 -0
  56. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/orchestration/temporal.py +0 -0
  57. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/qa/__init__.py +0 -0
  58. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/qa/validator.py +0 -0
  59. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/quality.py +0 -0
  60. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/query.py +0 -0
  61. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/scripts/__init__.py +0 -0
  62. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/scripts/post_install.py +0 -0
  63. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/server.py +0 -0
  64. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/streaming.py +0 -0
  65. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/webhook_handlers.py +0 -0
  66. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/python/pystreammcp/webhook_router.py +0 -0
  67. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/setup.cfg +0 -0
  68. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/setup.py +0 -0
  69. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_integration_phase2.py +0 -0
  70. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_langchain_integration.py +0 -0
  71. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_llamaindex_integration.py +0 -0
  72. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_mcp_connector_discovery.py +0 -0
  73. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_mcp_tools_handler.py +0 -0
  74. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_okf_core.py +0 -0
  75. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_okf_discovery.py +0 -0
  76. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_okf_query_planner.py +0 -0
  77. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_phase3_advanced.py +0 -0
  78. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_phase3_items12.py +0 -0
  79. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_phase3_items345.py +0 -0
  80. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_sprint3_rag_frameworks.py +0 -0
  81. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_sprint4_agent_frameworks.py +0 -0
  82. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_sprint5_orchestration.py +0 -0
  83. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_statguardian_integration.py +0 -0
  84. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_webhook_hmac_auth.py +0 -0
  85. {pystreammcp-3.2.0 → pystreammcp-3.3.0}/tests/test_webhook_router.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyStreamMCP
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: Intelligent MCP orchestration hub - Intent understanding, capability matching, tool ranking with cohesive foundation
5
5
  Author-email: Georgi Mammen Mullassery <mullassery@gmail.com>
6
6
  License: Proprietary License — Free to use with explicit attribution
@@ -91,7 +91,11 @@ pip install "PyStreamMCP[semantic-kernel]" # Semantic Kernel adapter
91
91
  pip install "PyStreamMCP[all-integrations]" # every framework adapter
92
92
  ```
93
93
 
94
- Requires Python 3.9+.
94
+ Requires Python 3.9+ for the core SDK (`Agent`, `SourceRegistry`,
95
+ `Orchestrator`). The `mcp` and `semantic-kernel` extras require Python
96
+ 3.10+ — their upstream packages don't publish Python 3.9-compatible
97
+ releases, so `pip install` for those extras will fail on 3.9. CI tests
98
+ 3.10, 3.11, and 3.12 only; 3.9 is not covered by CI.
95
99
 
96
100
  ## Quick start
97
101
 
@@ -120,6 +124,33 @@ for a full runnable walkthrough, including framework adapters for
120
124
  LangChain, LlamaIndex, CrewAI, Semantic Kernel, PydanticAI, and Haystack
121
125
  under [`pystreammcp.integrations`](python/pystreammcp/integrations/).
122
126
 
127
+ ## Data source discovery
128
+
129
+ `SourceRegistry` (`pystreammcp.SourceRegistry`) ranks *registered* data
130
+ sources by real word-overlap relevance to a query — not a fabricated or
131
+ hardcoded result:
132
+
133
+ ```python
134
+ from pystreammcp import SourceRegistry
135
+
136
+ registry = SourceRegistry()
137
+ registry.register(
138
+ "orders_db",
139
+ description="customer order history and revenue by quarter",
140
+ type="database",
141
+ tags=["orders", "revenue"],
142
+ )
143
+
144
+ results = registry.discover("customer revenue by quarter")
145
+ # [{"name": "orders_db", "type": "database", "relevance": 0.4444, "matched_terms": [...]}]
146
+ ```
147
+
148
+ The same registry backs `POST /discover` (REST), the `pystreammcp_discover`
149
+ MCP tool, and `LangchainAdapter.discover()` — register sources once via
150
+ `POST /sources` / `pystreammcp_register_source` / `registry.register()`,
151
+ then discover against them from any of the three surfaces. This is a
152
+ simple Jaccard token-overlap heuristic, not semantic/embedding search.
153
+
123
154
  ## MCP orchestration & federation
124
155
 
125
156
  `Orchestrator` (`pystreammcp.Orchestrator`) discovers and routes across
@@ -217,6 +248,49 @@ pip install -e ".[dev,api,mcp,langchain,llamaindex,semantic-kernel]"
217
248
  pytest tests/ -v
218
249
  ```
219
250
 
251
+ Note: this exact command needs Python 3.10+ (see [Install](#install)); on
252
+ 3.9 the `mcp` and `semantic-kernel` extras fail to resolve, so drop them
253
+ from the extras list if you're on 3.9.
254
+
255
+ ## Known Issues
256
+
257
+ - ~~No end-to-end tests for LLM failure states~~ **Fixed.** PyStreamMCP's
258
+ own code never calls a model provider directly (no code path here to add
259
+ `except RateLimitError`/`APIError` handling *to* — see `AgentFrameworkAdapter`
260
+ below), so what's now tested is the part this repo actually owns: that the
261
+ MCP tool-call path doesn't crash when whatever it's orchestrating fails.
262
+ `pystreammcp.testing` exports `RateLimitError`, `ContextWindowExceededError`,
263
+ `MalformedModelResponseError`, and `FailingAgent`/`FailingAdapter` stand-ins
264
+ that raise them; `mcp_server.py`'s `call_tool`/`process_message` now catch
265
+ exceptions raised during tool execution and return a structured
266
+ `{"status": "error", "error_type": ..., "message": ...}` response instead
267
+ of propagating uncaught. See `tests/test_mcp_server_resilience.py`.
268
+ - ~~No Pydantic validation for the MCP tool protocol itself~~ **Fixed.**
269
+ Each of the four MCP tools now has a Pydantic arg model
270
+ (`QueryToolArgs`/`DiscoverToolArgs`/`RegisterSourceToolArgs`/`OptimizeToolArgs`
271
+ in `mcp_server.py`) validated in `call_tool` before dispatch (wrong types,
272
+ invalid enum values, and missing required fields all now return a
273
+ structured `validation_errors` response instead of reaching the handler
274
+ unchecked), and `MCPTool.input_schema` is generated from those same models
275
+ via `model_json_schema()` — the advertised schema and the schema actually
276
+ enforced can no longer drift apart the way a hand-duplicated dict could.
277
+ - ~~No shipped mock MCP server / mock LLM fixture~~ **Fixed.** Added
278
+ `tests/conftest.py` (centralizing the `sys.path` setup 5 test files were
279
+ duplicating, plus `mock_adapter`/`failing_agent`/`failing_adapter`/
280
+ `llm_failure` pytest fixtures) and `pystreammcp/testing.py`, exporting
281
+ `MockAdapter` (promoted from the old test-local copy) and the
282
+ failure-injection stand-ins above for downstream projects' own test
283
+ suites to import directly.
284
+ - **Per-query context discovery** now has a real implementation
285
+ (`pystreammcp.discovery.SourceRegistry`): register data sources (name,
286
+ description, type, tags), and `/discover` (REST), `pystreammcp_discover`
287
+ (MCP), and `LangchainAdapter.discover()` all rank *registered* sources
288
+ by real Jaccard token-overlap against the query — an empty registry or
289
+ a non-matching context returns an empty list, not fabricated data. This
290
+ is a simple word-overlap heuristic, not semantic/embedding search — if
291
+ you need that, wrap `SourceRegistry` with your own similarity scoring.
292
+ - No open GitHub issues as of this pass.
293
+
220
294
  ## Rust workspace (not shipped)
221
295
 
222
296
  `core/` and `python/src/lib.rs` are an in-progress Rust workspace intended
@@ -27,7 +27,11 @@ pip install "PyStreamMCP[semantic-kernel]" # Semantic Kernel adapter
27
27
  pip install "PyStreamMCP[all-integrations]" # every framework adapter
28
28
  ```
29
29
 
30
- Requires Python 3.9+.
30
+ Requires Python 3.9+ for the core SDK (`Agent`, `SourceRegistry`,
31
+ `Orchestrator`). The `mcp` and `semantic-kernel` extras require Python
32
+ 3.10+ — their upstream packages don't publish Python 3.9-compatible
33
+ releases, so `pip install` for those extras will fail on 3.9. CI tests
34
+ 3.10, 3.11, and 3.12 only; 3.9 is not covered by CI.
31
35
 
32
36
  ## Quick start
33
37
 
@@ -56,6 +60,33 @@ for a full runnable walkthrough, including framework adapters for
56
60
  LangChain, LlamaIndex, CrewAI, Semantic Kernel, PydanticAI, and Haystack
57
61
  under [`pystreammcp.integrations`](python/pystreammcp/integrations/).
58
62
 
63
+ ## Data source discovery
64
+
65
+ `SourceRegistry` (`pystreammcp.SourceRegistry`) ranks *registered* data
66
+ sources by real word-overlap relevance to a query — not a fabricated or
67
+ hardcoded result:
68
+
69
+ ```python
70
+ from pystreammcp import SourceRegistry
71
+
72
+ registry = SourceRegistry()
73
+ registry.register(
74
+ "orders_db",
75
+ description="customer order history and revenue by quarter",
76
+ type="database",
77
+ tags=["orders", "revenue"],
78
+ )
79
+
80
+ results = registry.discover("customer revenue by quarter")
81
+ # [{"name": "orders_db", "type": "database", "relevance": 0.4444, "matched_terms": [...]}]
82
+ ```
83
+
84
+ The same registry backs `POST /discover` (REST), the `pystreammcp_discover`
85
+ MCP tool, and `LangchainAdapter.discover()` — register sources once via
86
+ `POST /sources` / `pystreammcp_register_source` / `registry.register()`,
87
+ then discover against them from any of the three surfaces. This is a
88
+ simple Jaccard token-overlap heuristic, not semantic/embedding search.
89
+
59
90
  ## MCP orchestration & federation
60
91
 
61
92
  `Orchestrator` (`pystreammcp.Orchestrator`) discovers and routes across
@@ -153,6 +184,49 @@ pip install -e ".[dev,api,mcp,langchain,llamaindex,semantic-kernel]"
153
184
  pytest tests/ -v
154
185
  ```
155
186
 
187
+ Note: this exact command needs Python 3.10+ (see [Install](#install)); on
188
+ 3.9 the `mcp` and `semantic-kernel` extras fail to resolve, so drop them
189
+ from the extras list if you're on 3.9.
190
+
191
+ ## Known Issues
192
+
193
+ - ~~No end-to-end tests for LLM failure states~~ **Fixed.** PyStreamMCP's
194
+ own code never calls a model provider directly (no code path here to add
195
+ `except RateLimitError`/`APIError` handling *to* — see `AgentFrameworkAdapter`
196
+ below), so what's now tested is the part this repo actually owns: that the
197
+ MCP tool-call path doesn't crash when whatever it's orchestrating fails.
198
+ `pystreammcp.testing` exports `RateLimitError`, `ContextWindowExceededError`,
199
+ `MalformedModelResponseError`, and `FailingAgent`/`FailingAdapter` stand-ins
200
+ that raise them; `mcp_server.py`'s `call_tool`/`process_message` now catch
201
+ exceptions raised during tool execution and return a structured
202
+ `{"status": "error", "error_type": ..., "message": ...}` response instead
203
+ of propagating uncaught. See `tests/test_mcp_server_resilience.py`.
204
+ - ~~No Pydantic validation for the MCP tool protocol itself~~ **Fixed.**
205
+ Each of the four MCP tools now has a Pydantic arg model
206
+ (`QueryToolArgs`/`DiscoverToolArgs`/`RegisterSourceToolArgs`/`OptimizeToolArgs`
207
+ in `mcp_server.py`) validated in `call_tool` before dispatch (wrong types,
208
+ invalid enum values, and missing required fields all now return a
209
+ structured `validation_errors` response instead of reaching the handler
210
+ unchecked), and `MCPTool.input_schema` is generated from those same models
211
+ via `model_json_schema()` — the advertised schema and the schema actually
212
+ enforced can no longer drift apart the way a hand-duplicated dict could.
213
+ - ~~No shipped mock MCP server / mock LLM fixture~~ **Fixed.** Added
214
+ `tests/conftest.py` (centralizing the `sys.path` setup 5 test files were
215
+ duplicating, plus `mock_adapter`/`failing_agent`/`failing_adapter`/
216
+ `llm_failure` pytest fixtures) and `pystreammcp/testing.py`, exporting
217
+ `MockAdapter` (promoted from the old test-local copy) and the
218
+ failure-injection stand-ins above for downstream projects' own test
219
+ suites to import directly.
220
+ - **Per-query context discovery** now has a real implementation
221
+ (`pystreammcp.discovery.SourceRegistry`): register data sources (name,
222
+ description, type, tags), and `/discover` (REST), `pystreammcp_discover`
223
+ (MCP), and `LangchainAdapter.discover()` all rank *registered* sources
224
+ by real Jaccard token-overlap against the query — an empty registry or
225
+ a non-matching context returns an empty list, not fabricated data. This
226
+ is a simple word-overlap heuristic, not semantic/embedding search — if
227
+ you need that, wrap `SourceRegistry` with your own similarity scoring.
228
+ - No open GitHub issues as of this pass.
229
+
156
230
  ## Rust workspace (not shipped)
157
231
 
158
232
  `core/` and `python/src/lib.rs` are an in-progress Rust workspace intended
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "PyStreamMCP"
7
- version = "3.2.0"
7
+ version = "3.3.0"
8
8
  description = "Intelligent MCP orchestration hub - Intent understanding, capability matching, tool ranking with cohesive foundation"
9
9
  readme = "README.md"
10
10
  license = {text = "Proprietary License — Free to use with explicit attribution"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyStreamMCP
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: Intelligent MCP orchestration hub - Intent understanding, capability matching, tool ranking with cohesive foundation
5
5
  Author-email: Georgi Mammen Mullassery <mullassery@gmail.com>
6
6
  License: Proprietary License — Free to use with explicit attribution
@@ -91,7 +91,11 @@ pip install "PyStreamMCP[semantic-kernel]" # Semantic Kernel adapter
91
91
  pip install "PyStreamMCP[all-integrations]" # every framework adapter
92
92
  ```
93
93
 
94
- Requires Python 3.9+.
94
+ Requires Python 3.9+ for the core SDK (`Agent`, `SourceRegistry`,
95
+ `Orchestrator`). The `mcp` and `semantic-kernel` extras require Python
96
+ 3.10+ — their upstream packages don't publish Python 3.9-compatible
97
+ releases, so `pip install` for those extras will fail on 3.9. CI tests
98
+ 3.10, 3.11, and 3.12 only; 3.9 is not covered by CI.
95
99
 
96
100
  ## Quick start
97
101
 
@@ -120,6 +124,33 @@ for a full runnable walkthrough, including framework adapters for
120
124
  LangChain, LlamaIndex, CrewAI, Semantic Kernel, PydanticAI, and Haystack
121
125
  under [`pystreammcp.integrations`](python/pystreammcp/integrations/).
122
126
 
127
+ ## Data source discovery
128
+
129
+ `SourceRegistry` (`pystreammcp.SourceRegistry`) ranks *registered* data
130
+ sources by real word-overlap relevance to a query — not a fabricated or
131
+ hardcoded result:
132
+
133
+ ```python
134
+ from pystreammcp import SourceRegistry
135
+
136
+ registry = SourceRegistry()
137
+ registry.register(
138
+ "orders_db",
139
+ description="customer order history and revenue by quarter",
140
+ type="database",
141
+ tags=["orders", "revenue"],
142
+ )
143
+
144
+ results = registry.discover("customer revenue by quarter")
145
+ # [{"name": "orders_db", "type": "database", "relevance": 0.4444, "matched_terms": [...]}]
146
+ ```
147
+
148
+ The same registry backs `POST /discover` (REST), the `pystreammcp_discover`
149
+ MCP tool, and `LangchainAdapter.discover()` — register sources once via
150
+ `POST /sources` / `pystreammcp_register_source` / `registry.register()`,
151
+ then discover against them from any of the three surfaces. This is a
152
+ simple Jaccard token-overlap heuristic, not semantic/embedding search.
153
+
123
154
  ## MCP orchestration & federation
124
155
 
125
156
  `Orchestrator` (`pystreammcp.Orchestrator`) discovers and routes across
@@ -217,6 +248,49 @@ pip install -e ".[dev,api,mcp,langchain,llamaindex,semantic-kernel]"
217
248
  pytest tests/ -v
218
249
  ```
219
250
 
251
+ Note: this exact command needs Python 3.10+ (see [Install](#install)); on
252
+ 3.9 the `mcp` and `semantic-kernel` extras fail to resolve, so drop them
253
+ from the extras list if you're on 3.9.
254
+
255
+ ## Known Issues
256
+
257
+ - ~~No end-to-end tests for LLM failure states~~ **Fixed.** PyStreamMCP's
258
+ own code never calls a model provider directly (no code path here to add
259
+ `except RateLimitError`/`APIError` handling *to* — see `AgentFrameworkAdapter`
260
+ below), so what's now tested is the part this repo actually owns: that the
261
+ MCP tool-call path doesn't crash when whatever it's orchestrating fails.
262
+ `pystreammcp.testing` exports `RateLimitError`, `ContextWindowExceededError`,
263
+ `MalformedModelResponseError`, and `FailingAgent`/`FailingAdapter` stand-ins
264
+ that raise them; `mcp_server.py`'s `call_tool`/`process_message` now catch
265
+ exceptions raised during tool execution and return a structured
266
+ `{"status": "error", "error_type": ..., "message": ...}` response instead
267
+ of propagating uncaught. See `tests/test_mcp_server_resilience.py`.
268
+ - ~~No Pydantic validation for the MCP tool protocol itself~~ **Fixed.**
269
+ Each of the four MCP tools now has a Pydantic arg model
270
+ (`QueryToolArgs`/`DiscoverToolArgs`/`RegisterSourceToolArgs`/`OptimizeToolArgs`
271
+ in `mcp_server.py`) validated in `call_tool` before dispatch (wrong types,
272
+ invalid enum values, and missing required fields all now return a
273
+ structured `validation_errors` response instead of reaching the handler
274
+ unchecked), and `MCPTool.input_schema` is generated from those same models
275
+ via `model_json_schema()` — the advertised schema and the schema actually
276
+ enforced can no longer drift apart the way a hand-duplicated dict could.
277
+ - ~~No shipped mock MCP server / mock LLM fixture~~ **Fixed.** Added
278
+ `tests/conftest.py` (centralizing the `sys.path` setup 5 test files were
279
+ duplicating, plus `mock_adapter`/`failing_agent`/`failing_adapter`/
280
+ `llm_failure` pytest fixtures) and `pystreammcp/testing.py`, exporting
281
+ `MockAdapter` (promoted from the old test-local copy) and the
282
+ failure-injection stand-ins above for downstream projects' own test
283
+ suites to import directly.
284
+ - **Per-query context discovery** now has a real implementation
285
+ (`pystreammcp.discovery.SourceRegistry`): register data sources (name,
286
+ description, type, tags), and `/discover` (REST), `pystreammcp_discover`
287
+ (MCP), and `LangchainAdapter.discover()` all rank *registered* sources
288
+ by real Jaccard token-overlap against the query — an empty registry or
289
+ a non-matching context returns an empty list, not fabricated data. This
290
+ is a simple word-overlap heuristic, not semantic/embedding search — if
291
+ you need that, wrap `SourceRegistry` with your own similarity scoring.
292
+ - No open GitHub issues as of this pass.
293
+
220
294
  ## Rust workspace (not shipped)
221
295
 
222
296
  `core/` and `python/src/lib.rs` are an in-progress Rust workspace intended
@@ -31,6 +31,7 @@ python/pystreammcp/quality.py
31
31
  python/pystreammcp/query.py
32
32
  python/pystreammcp/server.py
33
33
  python/pystreammcp/streaming.py
34
+ python/pystreammcp/testing.py
34
35
  python/pystreammcp/webhook_handlers.py
35
36
  python/pystreammcp/webhook_router.py
36
37
  python/pystreammcp/integrations/__init__.py
@@ -57,10 +58,12 @@ python/pystreammcp/qa/__init__.py
57
58
  python/pystreammcp/qa/validator.py
58
59
  python/pystreammcp/scripts/__init__.py
59
60
  python/pystreammcp/scripts/post_install.py
61
+ tests/test_discovery.py
60
62
  tests/test_integration_phase2.py
61
63
  tests/test_langchain_integration.py
62
64
  tests/test_llamaindex_integration.py
63
65
  tests/test_mcp_connector_discovery.py
66
+ tests/test_mcp_server_resilience.py
64
67
  tests/test_mcp_tools_handler.py
65
68
  tests/test_okf_core.py
66
69
  tests/test_okf_discovery.py
@@ -7,12 +7,12 @@ Reduces token usage by 60-75% while maintaining quality.
7
7
  Includes StatGuardian integration for data quality validation.
8
8
  """
9
9
 
10
- __version__ = "3.2.0"
10
+ __version__ = "3.3.0"
11
11
 
12
12
  from .agent import Agent, QueryResult
13
13
  from .query import Query, QueryIntent, QueryConstraints
14
14
  from .context import Context, ContextType, ContextWindow
15
- from .discovery import Discovery, DiscoveredSource, SourceType
15
+ from .discovery import Discovery, DiscoveredSource, SourceType, SourceRegistry, DataSource
16
16
 
17
17
  # Optimization imports
18
18
  try:
@@ -79,6 +79,8 @@ __all__ = [
79
79
  "Discovery",
80
80
  "DiscoveredSource",
81
81
  "SourceType",
82
+ "SourceRegistry",
83
+ "DataSource",
82
84
  # Legacy optimization (optional, phase 1-2)
83
85
  *((["OptimizationStrategy"]) if OptimizationStrategy else []),
84
86
  *((["StrategyType"]) if StrategyType else []),
@@ -13,6 +13,7 @@ import logging
13
13
 
14
14
  from .adapters import AdapterRegistry, AdapterConfig, FrameworkType, QueryResult
15
15
  from .agent import Agent
16
+ from .discovery import SourceRegistry
16
17
 
17
18
 
18
19
  logger = logging.getLogger(__name__)
@@ -59,6 +60,15 @@ class DiscoveryResponse(BaseModel):
59
60
  total_sources: int
60
61
 
61
62
 
63
+ class SourceRegistration(BaseModel):
64
+ """Register a data source for discovery."""
65
+
66
+ name: str = Field(..., description="Unique source name")
67
+ description: str = Field(..., description="What this source contains, in plain text")
68
+ type: str = Field("unknown", description="Source type, e.g. database, api, docs")
69
+ tags: List[str] = Field(default_factory=list, description="Optional keyword tags")
70
+
71
+
62
72
  class OptimizationRequest(BaseModel):
63
73
  """Query optimization request."""
64
74
 
@@ -117,6 +127,7 @@ class PyStreamMCPAPI:
117
127
  self.title = title
118
128
  self.version = version
119
129
  self.agents: Dict[str, Agent] = {}
130
+ self.registry = SourceRegistry()
120
131
  self.app = self._create_app()
121
132
 
122
133
  def _create_app(self) -> FastAPI:
@@ -260,13 +271,45 @@ class PyStreamMCPAPI:
260
271
  }
261
272
 
262
273
  # Discovery Endpoints
274
+ @app.post("/sources", response_model=Dict[str, Any])
275
+ async def register_source(source: SourceRegistration):
276
+ """Register a data source so /discover can find it."""
277
+ self.registry.register(
278
+ name=source.name,
279
+ description=source.description,
280
+ type=source.type,
281
+ tags=source.tags,
282
+ )
283
+ return {"status": "success", "name": source.name}
284
+
285
+ @app.delete("/sources/{name}")
286
+ async def unregister_source(name: str):
287
+ """Remove a registered data source."""
288
+ removed = self.registry.unregister(name)
289
+ if not removed:
290
+ raise HTTPException(status_code=404, detail=f"Source {name} not found")
291
+ return {"status": "success", "name": name}
292
+
293
+ @app.get("/sources")
294
+ async def list_sources():
295
+ """List all registered data sources."""
296
+ return {
297
+ "status": "success",
298
+ "sources": [
299
+ {"name": s.name, "type": s.type, "tags": s.tags}
300
+ for s in self.registry.list_sources()
301
+ ],
302
+ }
303
+
263
304
  @app.post("/discover", response_model=DiscoveryResponse)
264
305
  async def discover(request: DiscoveryRequest):
265
- """Discover relevant data sources."""
266
- agent = self._get_agent(request.agent_id)
306
+ """Discover data sources relevant to `context`.
267
307
 
268
- # TODO: Implement discovery logic
269
- sources = []
308
+ Ranks *registered* sources (see POST /sources) by real token
309
+ overlap against `context`. Returns an empty list if nothing is
310
+ registered or nothing overlaps — never a fabricated result.
311
+ """
312
+ sources = self.registry.discover(request.context)
270
313
 
271
314
  return {
272
315
  "status": "success",
@@ -0,0 +1,216 @@
1
+ """Discovery types and logic for PyStreamMCP.
2
+
3
+ `SourceRegistry` is real discovery logic: register data sources (name,
4
+ description, type, tags), then find the ones most relevant to a query
5
+ context using word-overlap relevance scoring. This is deliberately a
6
+ simple, honest heuristic — Jaccard token overlap between the query context
7
+ and each source's registered description/tags/name — not a semantic or
8
+ embedding-based search. Sources with zero token overlap are excluded from
9
+ results rather than padded in with a fabricated score, so an empty
10
+ registry or a context that matches nothing returns no sources, not
11
+ placeholder data.
12
+
13
+ `Discovery` / `DiscoveredSource` / `SourceType` are the result types
14
+ `SourceRegistry.discover()` returns results as, so callers get real,
15
+ structured discovery results rather than a bare dict.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from dataclasses import dataclass, field
22
+ from datetime import datetime
23
+ from enum import Enum
24
+ from typing import Any, Dict, List, Optional, Set
25
+
26
+ _WORD_RE = re.compile(r"[a-z0-9]+")
27
+
28
+
29
+ def _tokenize(text: str) -> Set[str]:
30
+ return set(_WORD_RE.findall(text.lower()))
31
+
32
+
33
+ class SourceType(str, Enum):
34
+ """Type of data source."""
35
+
36
+ TABLE = "table"
37
+ INDEX = "index"
38
+ CACHE = "cache"
39
+ EXTERNAL = "external"
40
+ COMPUTED = "computed"
41
+ UNKNOWN = "unknown"
42
+
43
+
44
+ @dataclass
45
+ class DiscoveredSource:
46
+ """A discovered data source, returned by SourceRegistry.discover()."""
47
+
48
+ name: str
49
+ source_type: SourceType
50
+ relevance_score: float
51
+ estimated_tokens: int
52
+ freshness_score: float = 1.0
53
+ source_id: str = ""
54
+ created_at: datetime = field(default_factory=datetime.now)
55
+
56
+ def __post_init__(self):
57
+ if not self.source_id:
58
+ import uuid
59
+
60
+ self.source_id = str(uuid.uuid4())
61
+ self.relevance_score = max(0.0, min(1.0, self.relevance_score))
62
+ self.freshness_score = max(0.0, min(1.0, self.freshness_score))
63
+
64
+ def quality_score(self) -> float:
65
+ """Combined relevance and freshness score."""
66
+ return self.relevance_score * self.freshness_score
67
+
68
+ def with_freshness(self, score: float) -> "DiscoveredSource":
69
+ """Set freshness score."""
70
+ self.freshness_score = max(0.0, min(1.0, score))
71
+ return self
72
+
73
+
74
+ @dataclass
75
+ class Discovery:
76
+ """Discovery result for a query."""
77
+
78
+ query_id: str
79
+ discovered_sources: List[DiscoveredSource] = field(default_factory=list)
80
+ discovery_id: str = ""
81
+ created_at: datetime = field(default_factory=datetime.now)
82
+
83
+ def __post_init__(self):
84
+ if not self.discovery_id:
85
+ import uuid
86
+
87
+ self.discovery_id = str(uuid.uuid4())
88
+
89
+ def add_source(self, source: DiscoveredSource) -> "Discovery":
90
+ """Add a discovered source."""
91
+ self.discovered_sources.append(source)
92
+ return self
93
+
94
+ def total_available_tokens(self) -> int:
95
+ """Total tokens across all sources."""
96
+ return sum(s.estimated_tokens for s in self.discovered_sources)
97
+
98
+ def top_sources(self, limit: int = 5) -> List[DiscoveredSource]:
99
+ """Get top sources by quality score."""
100
+ sorted_sources = sorted(
101
+ self.discovered_sources, key=lambda s: s.quality_score(), reverse=True
102
+ )
103
+ return sorted_sources[:limit]
104
+
105
+ def high_relevance_sources(self, threshold: float = 0.8) -> List[DiscoveredSource]:
106
+ """Get high-relevance sources."""
107
+ return [s for s in self.discovered_sources if s.relevance_score >= threshold]
108
+
109
+
110
+ @dataclass
111
+ class DataSource:
112
+ """A data source registered with a SourceRegistry."""
113
+
114
+ name: str
115
+ description: str
116
+ type: str = "unknown"
117
+ tags: List[str] = field(default_factory=list)
118
+ metadata: Dict[str, Any] = field(default_factory=dict)
119
+ estimated_tokens: int = 0
120
+
121
+ def _text(self) -> str:
122
+ return " ".join([self.name, self.description, *self.tags])
123
+
124
+
125
+ class SourceRegistry:
126
+ """In-memory registry of data sources, with real relevance scoring."""
127
+
128
+ def __init__(self) -> None:
129
+ self._sources: Dict[str, DataSource] = {}
130
+
131
+ def register(
132
+ self,
133
+ name: str,
134
+ description: str,
135
+ type: str = "unknown",
136
+ tags: Optional[List[str]] = None,
137
+ metadata: Optional[Dict[str, Any]] = None,
138
+ estimated_tokens: int = 0,
139
+ ) -> DataSource:
140
+ """Register (or replace) a data source by name."""
141
+ source = DataSource(
142
+ name=name,
143
+ description=description,
144
+ type=type,
145
+ tags=list(tags) if tags else [],
146
+ metadata=dict(metadata) if metadata else {},
147
+ estimated_tokens=estimated_tokens,
148
+ )
149
+ self._sources[name] = source
150
+ return source
151
+
152
+ def unregister(self, name: str) -> bool:
153
+ """Remove a registered source. Returns True if it existed."""
154
+ return self._sources.pop(name, None) is not None
155
+
156
+ def list_sources(self) -> List[DataSource]:
157
+ return list(self._sources.values())
158
+
159
+ def discover(self, context: str, limit: int = 10) -> List[Dict[str, Any]]:
160
+ """Return registered sources ranked by real relevance to `context`.
161
+
162
+ Relevance is Jaccard token overlap: |context tokens ∩ source
163
+ tokens| / |context tokens ∪ source tokens|. Sources that share no
164
+ tokens with `context` are excluded, not returned with a made-up
165
+ low score. Returns plain dicts (the wire-format shape used by the
166
+ REST/MCP surfaces) — use `discover_typed` for `DiscoveredSource`
167
+ objects.
168
+ """
169
+ return [
170
+ {
171
+ "name": s.name,
172
+ "type": s.type,
173
+ "relevance": round(score, 4),
174
+ "matched_terms": matched,
175
+ }
176
+ for score, matched, s in self._score(context, limit)
177
+ ]
178
+
179
+ def discover_typed(self, query_id: str, context: str, limit: int = 10) -> Discovery:
180
+ """Same ranking as `discover`, returned as a `Discovery` result of
181
+ real `DiscoveredSource` objects instead of plain dicts."""
182
+ result = Discovery(query_id=query_id)
183
+ for score, _matched, s in self._score(context, limit):
184
+ try:
185
+ source_type = SourceType(s.type)
186
+ except ValueError:
187
+ source_type = SourceType.UNKNOWN
188
+ result.add_source(
189
+ DiscoveredSource(
190
+ name=s.name,
191
+ source_type=source_type,
192
+ relevance_score=score,
193
+ estimated_tokens=s.estimated_tokens,
194
+ )
195
+ )
196
+ return result
197
+
198
+ def _score(self, context: str, limit: int):
199
+ context_tokens = _tokenize(context)
200
+ if not context_tokens or not self._sources:
201
+ return []
202
+
203
+ scored = []
204
+ for source in self._sources.values():
205
+ source_tokens = _tokenize(source._text())
206
+ if not source_tokens:
207
+ continue
208
+ overlap = context_tokens & source_tokens
209
+ if not overlap:
210
+ continue
211
+ union = context_tokens | source_tokens
212
+ relevance = len(overlap) / len(union)
213
+ scored.append((relevance, sorted(overlap), source))
214
+
215
+ scored.sort(key=lambda item: item[0], reverse=True)
216
+ return scored[:limit]