langgraph-api 0.18.0.dev7__py3-none-any.whl → 0.18.0.dev9__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.
langgraph_api/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.18.0.dev7"
1
+ __version__ = "0.18.0.dev9"
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING
6
+ from urllib.parse import urlsplit
7
+
8
+ from langgraph_api.config import ANY_ORIGIN
9
+
10
+ if TYPE_CHECKING:
11
+ from langgraph_api.config.schemas import CorsConfig
12
+
13
+ LOOPBACK_HOSTS = frozenset({"localhost", "127.0.0.1", "::1"})
14
+ WEB_SCHEMES = frozenset({"http", "https"})
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class OriginPolicy:
19
+ listed: frozenset[str]
20
+ pattern: re.Pattern[str] | None
21
+
22
+ @classmethod
23
+ def of(cls, settings: CorsConfig) -> OriginPolicy:
24
+ regex = settings.get("allow_origin_regex")
25
+ return cls(
26
+ frozenset(settings.get("allow_origins", ())) - {ANY_ORIGIN},
27
+ re.compile(regex) if regex is not None else None,
28
+ )
29
+
30
+ def allows(self, origin: str | None) -> bool:
31
+ return (
32
+ origin is None
33
+ or origin in self.listed
34
+ or (self.pattern is not None and self.pattern.fullmatch(origin) is not None)
35
+ or _bare_origin_host(origin) in LOOPBACK_HOSTS
36
+ )
37
+
38
+
39
+ def _bare_origin_host(origin: str) -> str | None:
40
+ try:
41
+ parsed = urlsplit(origin)
42
+ except ValueError:
43
+ return None
44
+ if (
45
+ parsed.scheme not in WEB_SCHEMES
46
+ or "@" in parsed.netloc
47
+ or parsed.path
48
+ or parsed.query
49
+ or parsed.fragment
50
+ ):
51
+ return None
52
+ return parsed.hostname or None
@@ -1,22 +1,27 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from http import HTTPStatus
4
+
3
5
  from starlette.responses import Response
4
6
 
5
7
  from langgraph_api.api.mcp._handlers import (
8
+ create_error_response,
6
9
  handle_delete_request,
7
10
  handle_get_request,
8
11
  handle_post_request,
9
12
  )
13
+ from langgraph_api.api.mcp._origins import OriginPolicy
14
+ from langgraph_api.config import CORS_SETTINGS
10
15
  from langgraph_api.route import ApiRequest, ApiRoute
11
16
 
12
17
  __all__ = ["mcp_routes"]
13
18
 
19
+ ORIGIN_POLICY = OriginPolicy.of(CORS_SETTINGS)
20
+
14
21
 
15
22
  async def handle_mcp_endpoint(request: ApiRequest) -> Response:
16
- # MCP endpoint handler that implements the Streamable HTTP protocol.
17
- # Supports POST (JSON-RPC request) and DELETE (terminate session).
18
- # GET (streaming session resumption) and text/event-stream not yet supported.
19
- # Route request based on HTTP method
23
+ if not ORIGIN_POLICY.allows(request.headers.get("origin")):
24
+ return create_error_response("Origin is not allowed", HTTPStatus.FORBIDDEN)
20
25
  if request.method == "DELETE":
21
26
  return handle_delete_request()
22
27
  elif request.method == "GET":
@@ -24,11 +29,9 @@ async def handle_mcp_endpoint(request: ApiRequest) -> Response:
24
29
  elif request.method == "POST":
25
30
  return await handle_post_request(request)
26
31
  else:
27
- # Method not allowed
28
32
  return Response(status_code=405)
29
33
 
30
34
 
31
- # Define routes for the MCP endpoint
32
35
  mcp_routes = [
33
36
  ApiRoute("/mcp", handle_mcp_endpoint, methods=["GET", "POST", "DELETE"]),
34
37
  ]
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from collections.abc import Sequence
2
3
  from enum import StrEnum
3
4
  from os import environ, getenv
4
5
  from typing import TYPE_CHECKING, Annotated, Literal, TypeVar, cast
@@ -363,7 +364,10 @@ MOUNT_PREFIX: str | None = _validate_mount_prefix(
363
364
  or (HTTP_CONFIG.get("mount_prefix") if HTTP_CONFIG else None)
364
365
  )
365
366
 
366
- CORS_ALLOW_ORIGINS = env("CORS_ALLOW_ORIGINS", cast=CommaSeparatedStrings, default="*")
367
+ ANY_ORIGIN = "*"
368
+ CORS_ALLOW_ORIGINS = env(
369
+ "CORS_ALLOW_ORIGINS", cast=CommaSeparatedStrings, default=ANY_ORIGIN
370
+ )
367
371
  CORS_CONFIG = env(
368
372
  "CORS_CONFIG", cast=_parse.parse_schema(CorsConfig), default=None
369
373
  ) or (HTTP_CONFIG.get("cors") if HTTP_CONFIG else None)
@@ -406,12 +410,35 @@ CORS_CONFIG = env(
406
410
  }
407
411
  }
408
412
  """
409
- if (
410
- CORS_CONFIG is not None
411
- and CORS_ALLOW_ORIGINS != "*"
412
- and CORS_CONFIG.get("allow_origins") is None
413
- ):
414
- CORS_CONFIG["allow_origins"] = CORS_ALLOW_ORIGINS
413
+
414
+
415
+ def _cors_settings(
416
+ cors_config: CorsConfig | None, allow_origins: Sequence[str]
417
+ ) -> CorsConfig:
418
+ origins = list(allow_origins)
419
+ if cors_config is None:
420
+ return {
421
+ "allow_origins": origins,
422
+ "allow_credentials": ANY_ORIGIN not in origins,
423
+ "allow_methods": ["*"],
424
+ "allow_headers": ["*"],
425
+ "expose_headers": [
426
+ "x-pagination-total",
427
+ "x-pagination-next",
428
+ "content-location",
429
+ "location",
430
+ ],
431
+ }
432
+ keeps_its_own_origins = cors_config.get("allow_origins") is not None
433
+ wildcard_would_widen_regex = cors_config.get(
434
+ "allow_origin_regex"
435
+ ) is not None and origins == [ANY_ORIGIN]
436
+ if keeps_its_own_origins or wildcard_would_widen_regex:
437
+ return cors_config
438
+ return {**cors_config, "allow_origins": origins}
439
+
440
+
441
+ CORS_SETTINGS = _cors_settings(CORS_CONFIG, CORS_ALLOW_ORIGINS)
415
442
 
416
443
  # queue
417
444
 
@@ -794,6 +821,7 @@ traceblock.patch_requests()
794
821
  __all__ = [
795
822
  "AES_JSON_DISALLOWED_KEYS",
796
823
  "ALLOW_PRIVATE_NETWORK",
824
+ "ANY_ORIGIN",
797
825
  "API_VARIANT",
798
826
  "BG_JOB_HEARTBEAT",
799
827
  "BG_JOB_ISOLATED_LOOPS",
@@ -810,6 +838,7 @@ __all__ = [
810
838
  "CHECKPOINT_MAX_BATCH_SIZE",
811
839
  "CORS_ALLOW_ORIGINS",
812
840
  "CORS_CONFIG",
841
+ "CORS_SETTINGS",
813
842
  "CRON_SCHEDULER_BATCH_SIZE",
814
843
  "CRON_SCHEDULER_SLEEP_TIME",
815
844
  "DATABASE_URI",
@@ -19,6 +19,7 @@ USE_DURABILITY = LANGGRAPH_PY_MINOR >= (0, 6)
19
19
  # Not in public docs: LANGGRAPH_RUNTIME_EDITION is internal, set by packaging/entrypoint
20
20
  _RUNTIME_EDITION = os.getenv("LANGGRAPH_RUNTIME_EDITION", "inmem")
21
21
  IS_POSTGRES_BACKEND = _RUNTIME_EDITION == "postgres"
22
+ IS_INMEM_BACKEND = _RUNTIME_EDITION == "inmem"
22
23
  IS_POSTGRES_OR_GRPC_BACKEND = IS_POSTGRES_BACKEND
23
24
  # Server-side kill switch for the Protocol v2 event-streaming surface
24
25
  # (#3296). When false, the v2 routes are not registered (clients hit a
@@ -670,6 +670,7 @@ const StreamEventsPayload = z.object({
670
670
  // keeps the legacy ``on_chain_stream`` behavior used by the older
671
671
  // streaming endpoints.
672
672
  stream_protocol: z.enum(["v2", "v3"]).nullish(),
673
+ output_keys: z.union([z.string(), z.array(z.string())]).nullish(),
673
674
  });
674
675
 
675
676
  function reviveCommand(
@@ -798,6 +799,10 @@ async function* streamEventsRequest(
798
799
  ...config,
799
800
  signal,
800
801
  version: "v3",
802
+ outputKeys:
803
+ payload.output_keys === undefined
804
+ ? graph.outputChannels
805
+ : (payload.output_keys ?? undefined),
801
806
  interruptBefore,
802
807
  interruptAfter,
803
808
  ...(callbacks ? { callbacks } : {}),
@@ -7,42 +7,42 @@
7
7
  "format": "prettier --write ."
8
8
  },
9
9
  "dependencies": {
10
- "@hono/node-server": "^2.1.0",
11
- "@hono/zod-validator": "^0.9.0",
12
- "@langchain/core": "^1.2.3",
13
- "@langchain/langgraph": "^1.4.12",
10
+ "@hono/node-server": "^2.1.5",
11
+ "@hono/zod-validator": "^0.9.1",
12
+ "@langchain/core": "^1.2.18",
13
+ "@langchain/langgraph": "^1.5.0",
14
14
  "@langchain/langgraph-api": "^1.0.3",
15
15
  "@langchain/langgraph-checkpoint": "^1.1.5",
16
- "@langchain/langgraph-ui": "^1.4.3",
16
+ "@langchain/langgraph-ui": "^1.5.3",
17
17
  "@types/json-schema": "^7.0.15",
18
18
  "dedent": "^1.7.2",
19
19
  "exit-hook": "^5.1.0",
20
- "hono": "^4.13.7",
20
+ "hono": "^4.13.13",
21
21
  "p-queue": "^9.3.1",
22
22
  "p-retry": "^7.1.1",
23
23
  "typescript": "^6.0.3",
24
24
  "undici": "^7.29.1",
25
- "uuid": "^14.0.1",
25
+ "uuid": "^14.0.3",
26
26
  "winston": "^3.19.0",
27
- "zod": "^4.4.3"
27
+ "zod": "^4.6.7"
28
28
  },
29
29
  "resolutions": {
30
30
  "@hono/node-server": ">=2.0.5",
31
- "esbuild": "^0.28.1",
32
- "langsmith": ">=0.4.6",
31
+ "esbuild": "^0.28.2",
32
+ "langsmith": ">=0.11.0 <1.0.0",
33
33
  "vite": "^6.4.3"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@langchain/langgraph-sdk": "^1.5.4",
37
- "@types/node": "^26.1.1",
38
- "@types/react": "^19.2.17",
39
- "@types/react-dom": "^19.2.3",
40
- "esbuild": "^0.28.1",
41
- "jose": "^6.2.3",
37
+ "@types/node": "^26.6.5",
38
+ "@types/react": "^19.3.0",
39
+ "@types/react-dom": "^19.3.0",
40
+ "esbuild": "^0.28.2",
41
+ "jose": "^6.2.12",
42
42
  "postgres": "^3.4.9",
43
- "prettier": "^3.9.5",
44
- "vite": "^8.1.4",
45
- "vitest": "^5.0.0"
43
+ "prettier": "^3.9.10",
44
+ "vite": "^8.3.4",
45
+ "vitest": "^5.0.5"
46
46
  },
47
47
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
48
48
  }