mainsequence 9.0.15.dev57__py3-none-any.whl → 9.0.16.dev59__py3-none-any.whl
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.
- agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +10 -0
- mainsequence/client/models_helpers.py +47 -0
- mainsequence/server/fastapi.py +159 -3
- {mainsequence-9.0.15.dev57.dist-info → mainsequence-9.0.16.dev59.dist-info}/METADATA +3 -1
- {mainsequence-9.0.15.dev57.dist-info → mainsequence-9.0.16.dev59.dist-info}/RECORD +9 -9
- {mainsequence-9.0.15.dev57.dist-info → mainsequence-9.0.16.dev59.dist-info}/WHEEL +0 -0
- {mainsequence-9.0.15.dev57.dist-info → mainsequence-9.0.16.dev59.dist-info}/entry_points.txt +0 -0
- {mainsequence-9.0.15.dev57.dist-info → mainsequence-9.0.16.dev59.dist-info}/licenses/LICENSE +0 -0
- {mainsequence-9.0.15.dev57.dist-info → mainsequence-9.0.16.dev59.dist-info}/top_level.txt +0 -0
|
@@ -211,6 +211,16 @@ what people are told:
|
|
|
211
211
|
See also
|
|
212
212
|
[Requester-bound calls](https://mainsequence-sdk.github.io/mainsequence-sdk/knowledge/fastapi/#requester-bound-calls).
|
|
213
213
|
|
|
214
|
+
### MCP tools
|
|
215
|
+
|
|
216
|
+
To serve MCP tools from the same application, add the `mcp` extra and call
|
|
217
|
+
`install_mcp(app, mcp.streamable_http_app(), lifespan=mcp.session_manager.run)`
|
|
218
|
+
after `install_request_identity(app)`, with a stateless FastMCP server that
|
|
219
|
+
returns JSON. Tools authorize with `User.get_logged_user()` and
|
|
220
|
+
`User.get_requester()` exactly like REST handlers; admission to the release
|
|
221
|
+
does not authorize every tool. See
|
|
222
|
+
[Serving MCP](https://mainsequence-sdk.github.io/mainsequence-sdk/knowledge/fastapi/#serving-mcp).
|
|
223
|
+
|
|
214
224
|
### Public provider callbacks and webhooks
|
|
215
225
|
|
|
216
226
|
An external OAuth redirect or webhook cannot supply a Main Sequence Bearer token.
|
|
@@ -822,6 +822,34 @@ class ResourceReleaseRuntimeAccess(BasePydanticModel):
|
|
|
822
822
|
access: dict[str, Any] | None = Field(default=None, repr=False)
|
|
823
823
|
|
|
824
824
|
|
|
825
|
+
class ResourceReleaseMcpConnection(BasePydanticModel):
|
|
826
|
+
"""How to connect to the MCP endpoint of a FastAPI release's active revision."""
|
|
827
|
+
|
|
828
|
+
url: str = Field(
|
|
829
|
+
...,
|
|
830
|
+
description="Public MCP endpoint: the release URL plus `/mcp`.",
|
|
831
|
+
examples=["https://prices.example.test/mcp"],
|
|
832
|
+
)
|
|
833
|
+
transport: OpenValueSet[Literal["streamable-http"]] = Field(
|
|
834
|
+
...,
|
|
835
|
+
description=(
|
|
836
|
+
"MCP transport of the endpoint. A value this SDK does not declare is kept "
|
|
837
|
+
"but is not a transport to connect with."
|
|
838
|
+
),
|
|
839
|
+
examples=["streamable-http"],
|
|
840
|
+
)
|
|
841
|
+
resource_metadata_url: str | None = Field(
|
|
842
|
+
None,
|
|
843
|
+
description="OAuth protected-resource metadata URL the endpoint's challenge points to.",
|
|
844
|
+
)
|
|
845
|
+
organization_environment_uid: str | None = Field(
|
|
846
|
+
None, description="Public UID of the connection's Organization Environment."
|
|
847
|
+
)
|
|
848
|
+
active_revision_uid: str | None = Field(
|
|
849
|
+
None, description="Public UID of the revision that advertises the endpoint."
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
|
|
825
853
|
class ResourceRelease(
|
|
826
854
|
CurrentCodeRepositoryBranchCollectionMixin,
|
|
827
855
|
EnvironmentLogSearchMixin,
|
|
@@ -853,6 +881,7 @@ class ResourceRelease(
|
|
|
853
881
|
"resource__uid": ["exact", "in"],
|
|
854
882
|
"related_job__uid": ["exact", "in"],
|
|
855
883
|
"release_kind": ["exact", "in"],
|
|
884
|
+
"mcp_available": ["exact"],
|
|
856
885
|
}
|
|
857
886
|
FILTER_VALUE_NORMALIZERS: ClassVar[dict[str, str]] = {
|
|
858
887
|
"uid": "uid",
|
|
@@ -861,6 +890,7 @@ class ResourceRelease(
|
|
|
861
890
|
"resource__uid": "uid",
|
|
862
891
|
"related_job__uid": "uid",
|
|
863
892
|
"release_kind": "str",
|
|
893
|
+
"mcp_available": "bool",
|
|
864
894
|
}
|
|
865
895
|
|
|
866
896
|
@classmethod
|
|
@@ -1005,6 +1035,23 @@ class ResourceRelease(
|
|
|
1005
1035
|
),
|
|
1006
1036
|
examples=["88888888-8888-4888-8888-888888888888"],
|
|
1007
1037
|
)
|
|
1038
|
+
mcp_enabled: bool = Field(
|
|
1039
|
+
False,
|
|
1040
|
+
title="MCP Enabled",
|
|
1041
|
+
description=(
|
|
1042
|
+
"Desired MCP capability of a FastAPI release, applied by its next successful "
|
|
1043
|
+
"deployment. False when the response does not carry it."
|
|
1044
|
+
),
|
|
1045
|
+
examples=[True],
|
|
1046
|
+
)
|
|
1047
|
+
mcp_connection: ResourceReleaseMcpConnection | None = Field(
|
|
1048
|
+
None,
|
|
1049
|
+
title="MCP Connection",
|
|
1050
|
+
description=(
|
|
1051
|
+
"MCP endpoint of the active MCP-enabled revision; None when no active revision "
|
|
1052
|
+
"advertises one. An advertised capability, not a health check."
|
|
1053
|
+
),
|
|
1054
|
+
)
|
|
1008
1055
|
observability: ObservabilityLinks | None = Field(
|
|
1009
1056
|
default=None,
|
|
1010
1057
|
description="Backend-owned runtime observability and related-history capabilities.",
|
mainsequence/server/fastapi.py
CHANGED
|
@@ -6,12 +6,18 @@ import asyncio
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import re
|
|
9
|
-
from contextlib import contextmanager
|
|
9
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
10
|
+
from urllib.parse import urlsplit
|
|
10
11
|
from uuid import UUID
|
|
11
12
|
|
|
12
13
|
import requests
|
|
13
14
|
|
|
14
|
-
from mainsequence._request_identity import
|
|
15
|
+
from mainsequence._request_identity import (
|
|
16
|
+
RequestIdentityError,
|
|
17
|
+
_get_request_identity,
|
|
18
|
+
_reads_as_caller,
|
|
19
|
+
_request_scope,
|
|
20
|
+
)
|
|
15
21
|
from mainsequence.client.models_user import RequestUserIdentity
|
|
16
22
|
|
|
17
23
|
from .caller_assertions import (
|
|
@@ -296,4 +302,154 @@ def reads_as_caller():
|
|
|
296
302
|
yield
|
|
297
303
|
|
|
298
304
|
|
|
299
|
-
|
|
305
|
+
_MCP_INSTALLATION = "mainsequence_mcp"
|
|
306
|
+
_MCP_PATH = "/mcp"
|
|
307
|
+
_WILDCARD_ORIGIN = re.compile(r"(https?)://\*\.([a-z0-9.-]+)(?::([0-9]{1,5}))?", flags=re.ASCII)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _mcp_allowed_origins():
|
|
311
|
+
"""The release's public origin and its CORS origins, read once at installation."""
|
|
312
|
+
entries = [os.environ.get("FASTAPI_PUBLIC_BASE_URL", "")]
|
|
313
|
+
entries += os.environ.get("FASTAPI_CORS_ALLOW_ORIGINS", "").split(",")
|
|
314
|
+
exact, patterns = set(), []
|
|
315
|
+
for entry in entries:
|
|
316
|
+
entry = entry.strip().rstrip("/").lower()
|
|
317
|
+
wildcard = _WILDCARD_ORIGIN.fullmatch(entry)
|
|
318
|
+
if wildcard:
|
|
319
|
+
scheme, host, port = wildcard.groups()
|
|
320
|
+
pattern = rf"{scheme}://[a-z0-9-]+\.{re.escape(host)}"
|
|
321
|
+
patterns.append(re.compile(pattern + (rf":{port}" if port else ""), flags=re.ASCII))
|
|
322
|
+
continue
|
|
323
|
+
parts = urlsplit(entry)
|
|
324
|
+
if parts.scheme in {"http", "https"} and parts.netloc and "*" not in parts.netloc:
|
|
325
|
+
exact.add(f"{parts.scheme}://{parts.netloc}")
|
|
326
|
+
return frozenset(exact), tuple(patterns)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _mcp_conflicts(routes, *, installed=None):
|
|
330
|
+
for route in routes:
|
|
331
|
+
path = getattr(route, "path", None)
|
|
332
|
+
if route is not installed and isinstance(path, str):
|
|
333
|
+
if path == _MCP_PATH or path.startswith(_MCP_PATH + "/"):
|
|
334
|
+
raise RuntimeError(f"Route {path} conflicts with the MCP endpoint at {_MCP_PATH}.")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
async def _reply(send, status, detail, headers=()):
|
|
338
|
+
await send(
|
|
339
|
+
{
|
|
340
|
+
"type": "http.response.start",
|
|
341
|
+
"status": status,
|
|
342
|
+
"headers": [
|
|
343
|
+
(b"content-type", b"application/json"),
|
|
344
|
+
(b"cache-control", b"no-store"),
|
|
345
|
+
*headers,
|
|
346
|
+
],
|
|
347
|
+
}
|
|
348
|
+
)
|
|
349
|
+
await send({"type": "http.response.body", "body": json.dumps({"detail": detail}).encode()})
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class _McpEndpoint:
|
|
353
|
+
"""Admit authenticated POSTs from an allowed Origin to the application's MCP app."""
|
|
354
|
+
|
|
355
|
+
def __init__(self, app, *, allowed_origins):
|
|
356
|
+
self.app = app
|
|
357
|
+
self.exact_origins, self.origin_patterns = allowed_origins
|
|
358
|
+
|
|
359
|
+
def _origin_allowed(self, origin):
|
|
360
|
+
origin = origin.strip().rstrip("/").lower()
|
|
361
|
+
return origin in self.exact_origins or any(
|
|
362
|
+
pattern.fullmatch(origin) for pattern in self.origin_patterns
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
async def __call__(self, scope, receive, send):
|
|
366
|
+
if scope.get("method") != "POST":
|
|
367
|
+
# Stateless profile: no standalone GET stream and no session to delete.
|
|
368
|
+
await _reply(send, 405, "Method Not Allowed.", headers=[(b"allow", b"POST")])
|
|
369
|
+
return
|
|
370
|
+
origins = [value for key, value in scope.get("headers", ()) if key.lower() == b"origin"]
|
|
371
|
+
if len(origins) > 1 or (origins and not self._origin_allowed(origins[0].decode("latin-1"))):
|
|
372
|
+
await _reply(send, 403, "Origin is not allowed.")
|
|
373
|
+
return
|
|
374
|
+
try:
|
|
375
|
+
_get_request_identity()
|
|
376
|
+
except RequestIdentityError:
|
|
377
|
+
await _reply(send, 401, "Authenticated request user could not be resolved.")
|
|
378
|
+
return
|
|
379
|
+
# Stateless profile: a session identifier never selects server-side state.
|
|
380
|
+
scope = dict(scope)
|
|
381
|
+
scope["headers"] = [
|
|
382
|
+
(key, value)
|
|
383
|
+
for key, value in scope.get("headers", ())
|
|
384
|
+
if key.lower() != b"mcp-session-id"
|
|
385
|
+
]
|
|
386
|
+
await self.app(scope, receive, send)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def install_mcp(app, mcp_app, *, lifespan):
|
|
390
|
+
"""Serve an application-owned MCP server at ``/mcp`` inside request identity.
|
|
391
|
+
|
|
392
|
+
``mcp_app`` is the server's ASGI application, answering Streamable HTTP at
|
|
393
|
+
``/mcp`` statelessly with JSON responses. ``lifespan`` is a zero-argument
|
|
394
|
+
callable returning its async context manager. With the official MCP Python
|
|
395
|
+
SDK, pass ``mcp.streamable_http_app()`` and ``mcp.session_manager.run``.
|
|
396
|
+
|
|
397
|
+
Install request identity first. Tools read the caller with
|
|
398
|
+
``User.get_logged_user()`` and, on a requester-bound call, the person it
|
|
399
|
+
works for with ``User.get_requester()``. Only authenticated POSTs reach the
|
|
400
|
+
server; other methods answer 405 and an ``Origin`` other than the release's
|
|
401
|
+
own or its CORS origins answers 403. See ADR 0038.
|
|
402
|
+
"""
|
|
403
|
+
from starlette.routing import Route
|
|
404
|
+
|
|
405
|
+
identity = getattr(app.state, _INSTALLATION, None)
|
|
406
|
+
if not isinstance(identity, dict) or identity.get("installed") is not True:
|
|
407
|
+
raise RuntimeError(
|
|
408
|
+
"Install request identity before MCP: call install_request_identity(app)."
|
|
409
|
+
)
|
|
410
|
+
if getattr(app.state, _MCP_INSTALLATION, None) is not None:
|
|
411
|
+
raise RuntimeError("MCP is already installed.")
|
|
412
|
+
if any(
|
|
413
|
+
path == _MCP_PATH or path.startswith(_MCP_PATH + "/")
|
|
414
|
+
for _, path in identity.get("public_ingress", ())
|
|
415
|
+
):
|
|
416
|
+
raise RuntimeError(f"{_MCP_PATH} cannot be public ingress; MCP requests are authenticated.")
|
|
417
|
+
if not callable(mcp_app) or not callable(lifespan):
|
|
418
|
+
raise TypeError("install_mcp needs the MCP ASGI app and its lifespan factory.")
|
|
419
|
+
_mcp_conflicts(app.router.routes)
|
|
420
|
+
|
|
421
|
+
route = Route(
|
|
422
|
+
_MCP_PATH,
|
|
423
|
+
endpoint=_McpEndpoint(mcp_app, allowed_origins=_mcp_allowed_origins()),
|
|
424
|
+
include_in_schema=False,
|
|
425
|
+
)
|
|
426
|
+
app.router.routes.insert(0, route)
|
|
427
|
+
application_lifespan = app.router.lifespan_context
|
|
428
|
+
started = False
|
|
429
|
+
|
|
430
|
+
@asynccontextmanager
|
|
431
|
+
async def composed_lifespan(application):
|
|
432
|
+
nonlocal started
|
|
433
|
+
if started:
|
|
434
|
+
raise RuntimeError("The MCP lifespan runs once per application.")
|
|
435
|
+
started = True
|
|
436
|
+
_mcp_conflicts(app.router.routes, installed=route)
|
|
437
|
+
async with application_lifespan(application) as state:
|
|
438
|
+
async with lifespan():
|
|
439
|
+
yield state
|
|
440
|
+
|
|
441
|
+
app.router.lifespan_context = composed_lifespan
|
|
442
|
+
setattr(
|
|
443
|
+
app.state,
|
|
444
|
+
_MCP_INSTALLATION,
|
|
445
|
+
{
|
|
446
|
+
"installed": True,
|
|
447
|
+
"path": _MCP_PATH,
|
|
448
|
+
"transport": "streamable-http",
|
|
449
|
+
"stateless": True,
|
|
450
|
+
"request_identity": True,
|
|
451
|
+
},
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
__all__ = ["install_mcp", "install_request_identity", "reads_as_caller"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mainsequence
|
|
3
|
-
Version: 9.0.
|
|
3
|
+
Version: 9.0.16.dev59
|
|
4
4
|
Summary: Main Sequence SDK
|
|
5
5
|
Author-email: Main Sequence GmbH <dev@main-sequence.io>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,6 +31,8 @@ Requires-Dist: requests>=2.33.0
|
|
|
31
31
|
Requires-Dist: structlog
|
|
32
32
|
Requires-Dist: typer
|
|
33
33
|
Requires-Dist: urllib3>=2.7.0
|
|
34
|
+
Provides-Extra: mcp
|
|
35
|
+
Requires-Dist: mcp<2,>=1.28; extra == "mcp"
|
|
34
36
|
Dynamic: license-file
|
|
35
37
|
|
|
36
38
|
<p align="center">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
agent_scaffold/AGENTS.md,sha256=-tK1zUP6j3QAlFXmwFVSNPfbzchZRJSzj26L-RU9Gis,6471
|
|
2
2
|
agent_scaffold/skills/a2a_sdk_execution/SKILL.md,sha256=NKtcXibE8TxZzKzz1KzJ2cVZ_EeuJJkb660U6wLvZ3s,11809
|
|
3
|
-
agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md,sha256=
|
|
3
|
+
agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md,sha256=1lPnRtm6qVaqOu_lJkk8V7HJ1NjKsp0hb8LB9xwzPOI,19608
|
|
4
4
|
agent_scaffold/skills/mainsequence-inference/SKILL.md,sha256=yv8aAHLQ2FBBSWPIFadY7pGNTwtR-yiR_-UzaGRN9-g,12502
|
|
5
5
|
agent_scaffold/skills/maintenance/bug_auditor/SKILL.md,sha256=CCKs7edyZ9RK-WtAgXJXucokJFXZxk8sRUe0_imFtu8,8610
|
|
6
6
|
agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md,sha256=pPsZaERlfokSJ6b6vLjpA5U0KuKxbOFzzcxOVW_ikiI,17935
|
|
@@ -44,7 +44,7 @@ mainsequence/client/exceptions.py,sha256=xx2L4l3OMNsTybbYkiTKK398ahnutEascM0It35
|
|
|
44
44
|
mainsequence/client/github_issues.py,sha256=pSTUw3UPrmE6wqnpoJctartTaNSq6iu6PQRrd8hip6I,14568
|
|
45
45
|
mainsequence/client/inference.py,sha256=tYKHRxLR6mA5iLmgd3qURwweWc2OPQuw0__tD3LQgSI,7748
|
|
46
46
|
mainsequence/client/models_foundry.py,sha256=JwQdPLDSO4dZI-KC0SUdmN6izmNIKMb9IzZlYXy0Mig,36488
|
|
47
|
-
mainsequence/client/models_helpers.py,sha256=
|
|
47
|
+
mainsequence/client/models_helpers.py,sha256=CQRjBjx3DsREJLRk0vqFsC-Dcll5lUP2ofu6e5UMvhk,49051
|
|
48
48
|
mainsequence/client/models_user.py,sha256=tOngVHxkoEeV-kLLK3cmfESN9gzyXrLlZHFNGVv014Q,45241
|
|
49
49
|
mainsequence/client/observability.py,sha256=_WrDKCaNk_TXmKElKCXC1inork6Exxxe8FRY4m1GI9s,16008
|
|
50
50
|
mainsequence/client/utils.py,sha256=C0h0U-fOBZw3UxIwHcBTLf8CDClAOatf_oRgjvqFth4,31637
|
|
@@ -53,10 +53,10 @@ mainsequence/instrumentation/__init__.py,sha256=J_9XmfjprElvz0u2t9rTyJT0kFdyKpxd
|
|
|
53
53
|
mainsequence/instrumentation/utils.py,sha256=b-ihjSioAepeLfMpudM2SqCtvnxMqIXileHx3klBCQU,3377
|
|
54
54
|
mainsequence/server/__init__.py,sha256=BHUAhtdWPDRdxX07NYO_6wrBv5mf0GsXflecO52PmUc,73
|
|
55
55
|
mainsequence/server/caller_assertions.py,sha256=Ix94Kvei31lKb861q6LqKzcB5H1mWnuH_wYwpXEKS1E,12351
|
|
56
|
-
mainsequence/server/fastapi.py,sha256=
|
|
57
|
-
mainsequence-9.0.
|
|
58
|
-
mainsequence-9.0.
|
|
59
|
-
mainsequence-9.0.
|
|
60
|
-
mainsequence-9.0.
|
|
61
|
-
mainsequence-9.0.
|
|
62
|
-
mainsequence-9.0.
|
|
56
|
+
mainsequence/server/fastapi.py,sha256=jB0EpxTqBw6sChtIGdPHGkC4I2MdV1V0uEd34NLvGQs,18267
|
|
57
|
+
mainsequence-9.0.16.dev59.dist-info/licenses/LICENSE,sha256=b6Ecsk8wjsEby3lbSzdljR6hHiFGHBizt9iWV2p1c_I,1074
|
|
58
|
+
mainsequence-9.0.16.dev59.dist-info/METADATA,sha256=k4UZhJV0jqz8epWSzNgtFkvEMOdf2mTJruMTCZNBn78,7467
|
|
59
|
+
mainsequence-9.0.16.dev59.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
60
|
+
mainsequence-9.0.16.dev59.dist-info/entry_points.txt,sha256=XGAOq3-mF2lWWpeRJvoaWyMarRcK167mH1g124aGdUs,88
|
|
61
|
+
mainsequence-9.0.16.dev59.dist-info/top_level.txt,sha256=BFeblM_Q9q0cUWBvTxnfWcWQEHtf5XZi9nOS-Gdfwus,28
|
|
62
|
+
mainsequence-9.0.16.dev59.dist-info/RECORD,,
|
|
File without changes
|
{mainsequence-9.0.15.dev57.dist-info → mainsequence-9.0.16.dev59.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{mainsequence-9.0.15.dev57.dist-info → mainsequence-9.0.16.dev59.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|