nlalpha-capability 0.2.0__tar.gz → 0.3.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nlalpha-capability
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: NLAlpha capability discovery MCP server.
5
5
  Project-URL: Homepage, https://github.com/ruimarj/nlalpha-capability
6
6
  Project-URL: Repository, https://github.com/ruimarj/nlalpha-capability
@@ -9,7 +9,7 @@ License-Expression: AGPL-3.0-or-later
9
9
  License-File: LICENSE
10
10
  Requires-Python: <3.13,>=3.12
11
11
  Requires-Dist: mcp<2.0,>=1.6.0
12
- Requires-Dist: nlalpha-core[json-log,telemetry]>=0.7.0
12
+ Requires-Dist: nlalpha-core[json-log,telemetry]>=0.7.1
13
13
  Requires-Dist: starlette>=0.37
14
14
  Requires-Dist: uvicorn>=0.30
15
15
  Description-Content-Type: text/markdown
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nlalpha-capability"
7
- version = "0.2.0"
7
+ version = "0.3.1"
8
8
  description = "NLAlpha capability discovery MCP server."
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
11
11
  license-files = ["LICENSE"]
12
12
  requires-python = ">=3.12,<3.13"
13
13
  dependencies = [
14
- "nlalpha-core[telemetry,json-log]>=0.7.0",
14
+ "nlalpha-core[telemetry,json-log]>=0.7.1",
15
15
  "mcp>=1.6.0,<2.0",
16
16
  "uvicorn>=0.30",
17
17
  "starlette>=0.37",
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  from contextlib import asynccontextmanager
6
7
  from dataclasses import asdict
7
8
  from importlib.metadata import version
@@ -26,24 +27,46 @@ from nlalpha.core.telemetry import configure_telemetry, shutdown, get_meter, set
26
27
 
27
28
  logger = logging.getLogger("nlalpha-capability")
28
29
 
30
+
31
+ @asynccontextmanager
32
+ async def _session_manager_lifespan(mcp):
33
+ stop = asyncio.Event()
34
+ started = asyncio.Event()
35
+
36
+ async def run():
37
+ async with mcp.session_manager.run():
38
+ started.set()
39
+ await stop.wait()
40
+
41
+ task = asyncio.create_task(run())
42
+ task.add_done_callback(lambda _: started.set())
43
+ await started.wait()
44
+ if task.done():
45
+ task.result()
46
+ try:
47
+ yield
48
+ finally:
49
+ stop.set()
50
+ await task
51
+
29
52
  _meter = get_meter()
30
53
  _domains_counter = _meter.create_counter("nlalpha.capability.domains.listed")
31
54
  _helpers_histogram = _meter.create_histogram("nlalpha.capability.helpers.count")
32
55
 
33
56
 
34
57
  def about(sections: list[str] | None = None) -> dict[str, Any]:
35
- """Describe the capability service, installed providers, helper domains, and families."""
58
+ """Start here: describe installed providers, helper domains, families, and diagnostics; task workflows use the strategy service's list_skills and load_skill."""
36
59
  with start_span("mcp.about"):
37
60
  try:
38
61
  return describe_document("service", "nlalpha-capability", "nlalpha-capability", version("nlalpha-capability"), {
39
62
  "principles": common_description(),
40
63
  "navigation": {
41
- "about": "Read this service's shared principles and installed capability directory.",
64
+ "about": "Start here: read shared principles, installed capability providers, and helper domains.",
42
65
  "list_capability_providers": "Discover selectable capability providers and their installed identities.",
43
66
  "list_capability_domains": "Discover domains from actual available helper declarations.",
44
67
  "list_capabilities_by_domain": "Read available helpers in a selected helper domain.",
45
68
  "list_family_helpers": "Read helpers from the same cached family contract used by strategy validation.",
46
- "confess": "Read a provider's manifest, methods, helpers, and declared guidance.",
69
+ "confess": "Read a provider's manifest, methods, helpers, and guidance before using its capabilities.",
47
70
  "diagnose_providers": "Inspect installed providers or families that failed discovery or are unavailable.",
48
71
  },
49
72
  "providers": CapabilityPluginRegistry.list_provider_records(),
@@ -56,7 +79,7 @@ def about(sections: list[str] | None = None) -> dict[str, Any]:
56
79
 
57
80
 
58
81
  def confess(provider_id: str, sections: list[str] | None = None) -> dict[str, Any]:
59
- """Describe a cached provider from its manifest, helpers, and required declared material."""
82
+ """Read a cached provider's manifest, helpers, and guidance before using its capabilities."""
60
83
  with start_span("mcp.confess", attributes={"provider_id": provider_id}):
61
84
  try:
62
85
  record = CapabilityPluginRegistry.get_record(provider_id)
@@ -182,24 +205,34 @@ async def _health(request):
182
205
  def create_app(*, allowed_hosts: list[str] | tuple[str, ...] | None = None,
183
206
  on_shutdown: Callable[[], None] | None = None) -> Starlette:
184
207
  """Create an independent MCP transport with an explicit host allowlist."""
185
- mcp = FastMCP("nlalpha-capability")
208
+ mcp = FastMCP(
209
+ "nlalpha-capability",
210
+ instructions=(
211
+ "NLAlpha capability MCP service. Start with `about` for navigation and `confess(provider_id)` "
212
+ "for provider semantics before using helpers. Task workflows are packaged as NLAlpha skills "
213
+ "in the strategy service: call `list_skills` then `load_skill` there when a workflow is "
214
+ "needed, one language per call."
215
+ ),
216
+ )
186
217
  mcp.settings.transport_security = TransportSecuritySettings(
187
218
  enable_dns_rebinding_protection=True, allowed_hosts=resolve_allowed_hosts(allowed_hosts),
188
219
  )
189
220
  for tool in (list_capability_domains, list_capabilities_by_domain, list_family_helpers,
190
221
  list_capability_providers, diagnose_providers, about, confess):
191
222
  mcp.add_tool(tool)
223
+ http_app = mcp.streamable_http_app()
224
+
192
225
  @asynccontextmanager
193
226
  async def lifespan(app):
194
227
  """Invoke only the explicitly injected process cleanup on application shutdown."""
195
- try:
196
- yield
197
- finally:
198
- if on_shutdown is not None:
199
- on_shutdown()
200
-
201
- sse_app = mcp.sse_app()
202
- app = Starlette(routes=[Route("/health", _health), *sse_app.routes], lifespan=lifespan)
228
+ async with _session_manager_lifespan(mcp):
229
+ try:
230
+ yield
231
+ finally:
232
+ if on_shutdown is not None:
233
+ on_shutdown()
234
+
235
+ app = Starlette(routes=[Route("/health", _health), *http_app.routes], lifespan=lifespan)
203
236
  app.state.mcp = mcp
204
237
  return app
205
238