meta-data-mcp 2.2.0__tar.gz → 2.3.2__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 (129) hide show
  1. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/PKG-INFO +3 -2
  2. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/README.md +1 -0
  3. meta_data_mcp-2.3.2/meta_data_mcp/__init__.py +1 -0
  4. meta_data_mcp-2.3.2/meta_data_mcp/oauth_provider.py +270 -0
  5. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/sdk/remote.py +37 -12
  6. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/server.py +246 -22
  7. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_news_tone_v1.html +9 -4
  8. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/pyproject.toml +1 -1
  9. meta_data_mcp-2.2.0/meta_data_mcp/__init__.py +0 -1
  10. meta_data_mcp-2.2.0/meta_data_mcp/sdk.py +0 -268
  11. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/.gitignore +0 -0
  12. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/AUTHORS +0 -0
  13. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/LICENSE +0 -0
  14. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/cli.py +0 -0
  15. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/client.py +0 -0
  16. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/discovery/__init__.py +0 -0
  17. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/discovery/loader.py +0 -0
  18. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/discovery/state.py +0 -0
  19. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/errors.py +0 -0
  20. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/fields.py +0 -0
  21. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/health.py +0 -0
  22. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/provenance.py +0 -0
  23. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/provider_config.py +0 -0
  24. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/__init__.py +0 -0
  25. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/__template__.py +0 -0
  26. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/au_data_gov.py +0 -0
  27. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/ca_open_gov.py +0 -0
  28. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/cern_opendata.py +0 -0
  29. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/ch_sbb.py +0 -0
  30. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/de_db.py +0 -0
  31. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/eu_copernicus.py +0 -0
  32. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/eu_ecb.py +0 -0
  33. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/eu_eurostat.py +0 -0
  34. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/fr_data_gouv.py +0 -0
  35. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_arxiv.py +0 -0
  36. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_bgpview.py +0 -0
  37. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_coingecko.py +0 -0
  38. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_crossref.py +0 -0
  39. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_dbnomics.py +0 -0
  40. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_disease_sh.py +0 -0
  41. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_epss.py +0 -0
  42. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_europepmc.py +0 -0
  43. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_faostat.py +0 -0
  44. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_frankfurter.py +0 -0
  45. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_gbif.py +0 -0
  46. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_gdelt.py +0 -0
  47. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_imf.py +0 -0
  48. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_inaturalist.py +0 -0
  49. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_met_museum.py +0 -0
  50. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_nvd_cve.py +0 -0
  51. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_oecd.py +0 -0
  52. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_open_library.py +0 -0
  53. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_open_meteo.py +0 -0
  54. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_openalex.py +0 -0
  55. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_openaq.py +0 -0
  56. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_opensanctions.py +0 -0
  57. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_opensky.py +0 -0
  58. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_osm_nominatim.py +0 -0
  59. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_osv_dev.py +0 -0
  60. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_overpass.py +0 -0
  61. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_pubchem.py +0 -0
  62. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_rcsb_pdb.py +0 -0
  63. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_ripe_stat.py +0 -0
  64. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_un_comtrade.py +0 -0
  65. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_unesco_heritage.py +0 -0
  66. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_who_gho.py +0 -0
  67. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_wikidata.py +0 -0
  68. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_wikipedia.py +0 -0
  69. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_world_bank.py +0 -0
  70. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/meta_data_mcp.py +0 -0
  71. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/nl_cbs.py +0 -0
  72. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/nl_ndov.py +0 -0
  73. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/nl_rechtspraak.py +0 -0
  74. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/nl_tweedekamer.py +0 -0
  75. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/sg_data_gov.py +0 -0
  76. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/uk_gov.py +0 -0
  77. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/uk_legislation.py +0 -0
  78. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/uk_ons.py +0 -0
  79. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_arcgis_item.py +0 -0
  80. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_cary.py +0 -0
  81. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_cdc_socrata.py +0 -0
  82. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_census_geocoder.py +0 -0
  83. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_cisa_kev.py +0 -0
  84. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_clinicaltrials.py +0 -0
  85. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_courtlistener.py +0 -0
  86. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_data_gov.py +0 -0
  87. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_faa_nasstatus.py +0 -0
  88. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_fayetteville.py +0 -0
  89. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_fda_openfda.py +0 -0
  90. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_federal_register.py +0 -0
  91. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_healthdata_gov.py +0 -0
  92. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_nasa.py +0 -0
  93. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_nc_onemap.py +0 -0
  94. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_ncdeq_gis.py +0 -0
  95. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_noaa_awc.py +0 -0
  96. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_noaa_ncei.py +0 -0
  97. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_noaa_tides.py +0 -0
  98. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_raleigh.py +0 -0
  99. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_sec_edgar.py +0 -0
  100. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_treasury_fiscal.py +0 -0
  101. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_usgs_earthquake.py +0 -0
  102. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/registry.py +0 -0
  103. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/routing.py +0 -0
  104. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/sdk/__init__.py +0 -0
  105. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/serialize.py +0 -0
  106. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/transport.py +0 -0
  107. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/__init__.py +0 -0
  108. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_discovery_v1.html +0 -0
  109. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_discovery_v1.py +0 -0
  110. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_entity_graph_v1.html +0 -0
  111. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_entity_graph_v1.py +0 -0
  112. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_molecular_v1.html +0 -0
  113. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_molecular_v1.py +0 -0
  114. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_museum_v1.html +0 -0
  115. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_museum_v1.py +0 -0
  116. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_network_topology_v1.html +0 -0
  117. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_network_topology_v1.py +0 -0
  118. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_news_tone_v1.py +0 -0
  119. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_trade_flows_v1.html +0 -0
  120. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_trade_flows_v1.py +0 -0
  121. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_vulnerability_v1.html +0 -0
  122. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_vulnerability_v1.py +0 -0
  123. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/shape_geofeatures_v1.html +0 -0
  124. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/shape_geofeatures_v1.py +0 -0
  125. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/shape_records_v1.html +0 -0
  126. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/shape_records_v1.py +0 -0
  127. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/shape_timeseries_v1.html +0 -0
  128. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/shape_timeseries_v1.py +0 -0
  129. {meta_data_mcp-2.2.0 → meta_data_mcp-2.3.2}/meta_data_mcp/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-data-mcp
3
- Version: 2.2.0
3
+ Version: 2.3.2
4
4
  Summary: MCP server exposing 60+ open data providers to LLM clients via intelligent metadata routing.
5
5
  Project-URL: Homepage, https://github.com/derekslinz/meta-data-mcp
6
6
  Project-URL: Repository, https://github.com/derekslinz/meta-data-mcp
@@ -41,7 +41,7 @@ Requires-Python: >=3.12
41
41
  Requires-Dist: anyio>=4.0.0
42
42
  Requires-Dist: click>=8.1.7
43
43
  Requires-Dist: httpx>=0.28.0
44
- Requires-Dist: mcp>=1.0.0
44
+ Requires-Dist: mcp>=1.9.0
45
45
  Requires-Dist: pydantic>=2.0.0
46
46
  Requires-Dist: pyyaml>=6.0
47
47
  Requires-Dist: starlette>=1.0.0
@@ -373,6 +373,7 @@ A few bundled plugins accept optional API keys for higher rate limits. Set these
373
373
  |---|---|
374
374
  | `META_DATA_MCP_PRELOAD` | Comma-separated plugin ids to activate at startup, or `*` for all. Default unset = discovery-only (~13 meta tools). |
375
375
  | `META_DATA_MCP_AUTH_TOKEN` | When set on the SSE transport, requires `Authorization: Bearer <token>` on `/sse` and `/messages`. |
376
+ | `META_DATA_MCP_OAUTH_ISSUER` | Enable OAuth 2.0 Authorization Code + PKCE. Set to the server's public base URL (e.g. `http://localhost:8000`). Mounts `/.well-known/oauth-authorization-server`, `/register`, `/authorize`, `/token`, `/revoke`, and a consent page at `/oauth/consent`. Coexists with `META_DATA_MCP_AUTH_TOKEN` — both auth methods remain valid simultaneously. |
376
377
  | `META_DATA_MCP_PROVENANCE` | Truthy (`1`, `true`, `yes`, `on`) enables a `meta-data-mcp/provenance` entry on every tool-call result's first content block's `_meta`, carrying `sha256` and `timestamp` (ISO 8601 UTC, ms precision). The digest covers the canonical `(tool, arguments, content)` envelope — content blocks dumped via `model_dump(mode="json", by_alias=True, exclude_none=True)` with `_meta` stripped, JSON-serialized with `sort_keys=True, separators=(",",":"), ensure_ascii=True`. Binding tool name + arguments into the hash means audit logs can distinguish "tool A returned X" from "tool B returned X". Default off — opt in when you need tamper-evidence. See `meta_data_mcp/provenance.py` module docstring for the verbatim receiver recipe. |
377
378
 
378
379
  ## Transports
@@ -323,6 +323,7 @@ A few bundled plugins accept optional API keys for higher rate limits. Set these
323
323
  |---|---|
324
324
  | `META_DATA_MCP_PRELOAD` | Comma-separated plugin ids to activate at startup, or `*` for all. Default unset = discovery-only (~13 meta tools). |
325
325
  | `META_DATA_MCP_AUTH_TOKEN` | When set on the SSE transport, requires `Authorization: Bearer <token>` on `/sse` and `/messages`. |
326
+ | `META_DATA_MCP_OAUTH_ISSUER` | Enable OAuth 2.0 Authorization Code + PKCE. Set to the server's public base URL (e.g. `http://localhost:8000`). Mounts `/.well-known/oauth-authorization-server`, `/register`, `/authorize`, `/token`, `/revoke`, and a consent page at `/oauth/consent`. Coexists with `META_DATA_MCP_AUTH_TOKEN` — both auth methods remain valid simultaneously. |
326
327
  | `META_DATA_MCP_PROVENANCE` | Truthy (`1`, `true`, `yes`, `on`) enables a `meta-data-mcp/provenance` entry on every tool-call result's first content block's `_meta`, carrying `sha256` and `timestamp` (ISO 8601 UTC, ms precision). The digest covers the canonical `(tool, arguments, content)` envelope — content blocks dumped via `model_dump(mode="json", by_alias=True, exclude_none=True)` with `_meta` stripped, JSON-serialized with `sort_keys=True, separators=(",",":"), ensure_ascii=True`. Binding tool name + arguments into the hash means audit logs can distinguish "tool A returned X" from "tool B returned X". Default off — opt in when you need tamper-evidence. See `meta_data_mcp/provenance.py` module docstring for the verbatim receiver recipe. |
327
328
 
328
329
  ## Transports
@@ -0,0 +1 @@
1
+ __version__ = "2.3.2"
@@ -0,0 +1,270 @@
1
+ """In-memory OAuth 2.0 Authorization Server provider for meta-data-mcp.
2
+
3
+ Implements the MCP SDK's ``OAuthAuthorizationServerProvider`` Protocol with
4
+ full in-memory storage (tokens are lost on server restart). Suitable for
5
+ development, single-node deployments, and operator-managed SSE servers.
6
+
7
+ Enable OAuth by setting ``META_DATA_MCP_OAUTH_ISSUER`` (e.g.
8
+ ``http://localhost:8000``). This coexists with the existing bearer-token
9
+ auth (``META_DATA_MCP_AUTH_TOKEN``) — both remain valid simultaneously.
10
+
11
+ Flow (Authorization Code + PKCE + Dynamic Client Registration):
12
+ 1. Client POSTs to ``/register`` → receives ``client_id`` + ``client_secret``
13
+ 2. Client opens ``/authorize?client_id=…&code_challenge=…&redirect_uri=…``
14
+ 3. User is redirected to ``/oauth/consent?session=…``
15
+ 4. User approves → ``/oauth/consent/approve`` creates an auth code and
16
+ redirects the browser to the client's ``redirect_uri?code=…&state=…``
17
+ 5. Client POSTs to ``/token`` with ``code`` + ``code_verifier`` → tokens
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import base64
23
+ import hashlib
24
+ import secrets
25
+ import time
26
+ from typing import Any
27
+
28
+ from mcp.server.auth.provider import (
29
+ AccessToken,
30
+ AuthorizationCode,
31
+ AuthorizationParams,
32
+ OAuthAuthorizationServerProvider,
33
+ RefreshToken,
34
+ )
35
+ from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
36
+
37
+
38
+ class InMemoryOAuthProvider(
39
+ OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]
40
+ ):
41
+ """Stateful in-memory OAuth provider.
42
+
43
+ All state is local to the process — tokens are lost on restart.
44
+ Thread-safe for asyncio (single-threaded event loop) but not safe for
45
+ multi-process deployments; use a shared cache (Redis/Postgres) for HA.
46
+ """
47
+
48
+ def __init__(self, issuer_url: str) -> None:
49
+ self.issuer_url = issuer_url.rstrip("/")
50
+ # Storage maps: key → object
51
+ self._clients: dict[str, OAuthClientInformationFull] = {}
52
+ self._auth_sessions: dict[str, dict[str, Any]] = {} # consent-page sessions
53
+ self._auth_codes: dict[str, AuthorizationCode] = {}
54
+ self._access_tokens: dict[str, AccessToken] = {}
55
+ self._refresh_tokens: dict[str, RefreshToken] = {}
56
+
57
+ # ------------------------------------------------------------------
58
+ # Client management
59
+ # ------------------------------------------------------------------
60
+
61
+ async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
62
+ return self._clients.get(client_id)
63
+
64
+ async def register_client(self, client_info: OAuthClientInformationFull) -> None:
65
+ if client_info.client_id is None:
66
+ raise ValueError("client_id is required")
67
+ self._clients[client_info.client_id] = client_info
68
+
69
+ # ------------------------------------------------------------------
70
+ # Authorization (consent page redirect)
71
+ # ------------------------------------------------------------------
72
+
73
+ async def authorize(
74
+ self, client: OAuthClientInformationFull, params: AuthorizationParams
75
+ ) -> str:
76
+ """Create a short-lived consent session and return the consent page URL."""
77
+ session_token = secrets.token_urlsafe(32)
78
+ self._auth_sessions[session_token] = {
79
+ "client_id": client.client_id,
80
+ "client_name": getattr(client, "client_name", client.client_id),
81
+ "code_challenge": params.code_challenge,
82
+ "redirect_uri": str(params.redirect_uri),
83
+ "redirect_uri_provided_explicitly": params.redirect_uri_provided_explicitly,
84
+ "scopes": params.scopes or [],
85
+ "state": params.state,
86
+ "expires_at": time.time() + 600, # 10-minute consent window
87
+ }
88
+ return f"{self.issuer_url}/oauth/consent?session={session_token}"
89
+
90
+ def peek_session(self, session_token: str) -> dict[str, Any] | None:
91
+ """Return a shallow copy of a pending consent session without consuming it.
92
+
93
+ Used by the GET consent page to display session details while leaving
94
+ the session intact for the POST approval step. Returns a shallow copy
95
+ so callers cannot accidentally mutate internal provider state.
96
+ Returns None for unknown or expired sessions.
97
+ """
98
+ session = self._auth_sessions.get(session_token)
99
+ if session is None:
100
+ return None
101
+ if time.time() > session["expires_at"]:
102
+ self._auth_sessions.pop(session_token, None)
103
+ return None
104
+ return dict(session) # shallow copy — callers cannot mutate stored state
105
+
106
+ def consume_session(self, session_token: str) -> dict[str, Any] | None:
107
+ """Retrieve and remove a pending consent session (one-shot)."""
108
+ session = self._auth_sessions.pop(session_token, None)
109
+ if session is None:
110
+ return None
111
+ if time.time() > session["expires_at"]:
112
+ return None
113
+ return session
114
+
115
+ def create_authorization_code(self, session: dict[str, Any]) -> str:
116
+ """Issue an authorization code from an approved consent session."""
117
+ code = secrets.token_urlsafe(32)
118
+ self._auth_codes[code] = AuthorizationCode(
119
+ code=code,
120
+ scopes=session["scopes"],
121
+ expires_at=time.time() + 300, # 5-minute code lifetime
122
+ client_id=session["client_id"],
123
+ code_challenge=session["code_challenge"],
124
+ redirect_uri=session["redirect_uri"], # type: ignore[arg-type]
125
+ redirect_uri_provided_explicitly=session[
126
+ "redirect_uri_provided_explicitly"
127
+ ],
128
+ )
129
+ return code
130
+
131
+ # ------------------------------------------------------------------
132
+ # Authorization code exchange
133
+ # ------------------------------------------------------------------
134
+
135
+ async def load_authorization_code(
136
+ self,
137
+ client: OAuthClientInformationFull,
138
+ authorization_code: str,
139
+ ) -> AuthorizationCode | None:
140
+ code = self._auth_codes.get(authorization_code)
141
+ if code is None:
142
+ return None
143
+ if time.time() > code.expires_at:
144
+ del self._auth_codes[authorization_code]
145
+ return None
146
+ if code.client_id != client.client_id:
147
+ return None
148
+ return code
149
+
150
+ async def exchange_authorization_code(
151
+ self,
152
+ client: OAuthClientInformationFull,
153
+ authorization_code: AuthorizationCode,
154
+ ) -> OAuthToken:
155
+ """Consume the authorization code and issue access + refresh tokens.
156
+
157
+ PKCE validation (code_verifier vs code_challenge) is performed by the
158
+ MCP SDK's ``TokenHandler`` before calling this method — this provider
159
+ does not repeat that check.
160
+ """
161
+ # Remove the used code (one-shot).
162
+ self._auth_codes.pop(authorization_code.code, None)
163
+
164
+ access_token_str = secrets.token_urlsafe(32)
165
+ refresh_token_str = secrets.token_urlsafe(32)
166
+
167
+ client_id = client.client_id or ""
168
+ self._access_tokens[access_token_str] = AccessToken(
169
+ token=access_token_str,
170
+ client_id=client_id,
171
+ scopes=authorization_code.scopes,
172
+ expires_at=int(time.time()) + 3600, # 1-hour access token
173
+ )
174
+ self._refresh_tokens[refresh_token_str] = RefreshToken(
175
+ token=refresh_token_str,
176
+ client_id=client_id,
177
+ scopes=authorization_code.scopes,
178
+ )
179
+
180
+ return OAuthToken(
181
+ access_token=access_token_str,
182
+ token_type="Bearer",
183
+ expires_in=3600,
184
+ refresh_token=refresh_token_str,
185
+ scope=" ".join(authorization_code.scopes),
186
+ )
187
+
188
+ # ------------------------------------------------------------------
189
+ # Refresh token exchange
190
+ # ------------------------------------------------------------------
191
+
192
+ async def load_refresh_token(
193
+ self,
194
+ client: OAuthClientInformationFull,
195
+ refresh_token: str,
196
+ ) -> RefreshToken | None:
197
+ rt = self._refresh_tokens.get(refresh_token)
198
+ if rt is None or rt.client_id != client.client_id:
199
+ return None
200
+ return rt
201
+
202
+ async def exchange_refresh_token(
203
+ self,
204
+ client: OAuthClientInformationFull,
205
+ refresh_token: RefreshToken,
206
+ scopes: list[str],
207
+ ) -> OAuthToken:
208
+ """Issue a new access token (and rotate the refresh token)."""
209
+ # Invalidate the old refresh token.
210
+ self._refresh_tokens.pop(refresh_token.token, None)
211
+
212
+ effective_scopes = scopes or refresh_token.scopes
213
+ new_access = secrets.token_urlsafe(32)
214
+ new_refresh = secrets.token_urlsafe(32)
215
+ client_id = client.client_id or ""
216
+
217
+ self._access_tokens[new_access] = AccessToken(
218
+ token=new_access,
219
+ client_id=client_id,
220
+ scopes=effective_scopes,
221
+ expires_at=int(time.time()) + 3600,
222
+ )
223
+ self._refresh_tokens[new_refresh] = RefreshToken(
224
+ token=new_refresh,
225
+ client_id=client_id,
226
+ scopes=effective_scopes,
227
+ )
228
+
229
+ return OAuthToken(
230
+ access_token=new_access,
231
+ token_type="Bearer",
232
+ expires_in=3600,
233
+ refresh_token=new_refresh,
234
+ scope=" ".join(effective_scopes),
235
+ )
236
+
237
+ # ------------------------------------------------------------------
238
+ # Access token verification
239
+ # ------------------------------------------------------------------
240
+
241
+ async def verify_access_token(self, token: str) -> AccessToken | None:
242
+ at = self._access_tokens.get(token)
243
+ if at is None:
244
+ return None
245
+ if at.expires_at is not None and time.time() > at.expires_at:
246
+ del self._access_tokens[token]
247
+ return None
248
+ return at
249
+
250
+ # ------------------------------------------------------------------
251
+ # Token revocation
252
+ # ------------------------------------------------------------------
253
+
254
+ async def revoke_token(
255
+ self,
256
+ token: AccessToken | RefreshToken,
257
+ ) -> None:
258
+ self._access_tokens.pop(token.token, None)
259
+ self._refresh_tokens.pop(token.token, None)
260
+
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # PKCE helper (used by tests; not part of the Protocol)
264
+ # ---------------------------------------------------------------------------
265
+
266
+
267
+ def compute_pkce_challenge(code_verifier: str) -> str:
268
+ """Return the S256 code_challenge for a given verifier."""
269
+ digest = hashlib.sha256(code_verifier.encode()).digest()
270
+ return base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
@@ -73,11 +73,30 @@ class RemoteClient:
73
73
  base_url: str,
74
74
  token: str | None = None,
75
75
  timeout: float = 30.0,
76
+ auth: Any = None,
76
77
  ) -> None:
78
+ """
79
+ Args:
80
+ base_url: Root URL of the server (e.g. ``"https://mcp.example.com"``).
81
+ The ``/sse`` path is appended automatically.
82
+ token: Bearer token for the server's auth middleware.
83
+ Falls back to ``META_DATA_MCP_AUTH_TOKEN`` env var.
84
+ Mutually exclusive with ``auth``.
85
+ timeout: Connection timeout in seconds (default 30 s).
86
+ auth: MCP-compatible OAuth client provider for Authorization Code
87
+ + PKCE flows. Accepts any object implementing the
88
+ ``mcp.client.sse`` ``auth=`` interface (e.g. a custom
89
+ ``OAuthClientProvider``). Mutually exclusive with ``token``.
90
+ """
91
+ if token is not None and auth is not None:
92
+ raise ValueError(
93
+ "token and auth are mutually exclusive — use one or the other"
94
+ )
77
95
  self._base_url = base_url.rstrip("/")
78
96
  self._token = (
79
97
  token if token is not None else os.getenv("META_DATA_MCP_AUTH_TOKEN")
80
98
  )
99
+ self._auth = auth # mcp-compatible OAuthClientProvider, or None
81
100
  self._timeout = timeout
82
101
  self._session: Any = None # mcp.client.session.ClientSession when active
83
102
  self._exit_stack: AsyncExitStack | None = None
@@ -91,13 +110,15 @@ class RemoteClient:
91
110
  from mcp.client.sse import sse_client
92
111
 
93
112
  self._exit_stack = AsyncExitStack()
94
- streams = await self._exit_stack.enter_async_context(
95
- sse_client(
96
- url=f"{self._base_url}/sse",
97
- headers=self._headers(),
98
- timeout=self._timeout,
99
- )
100
- )
113
+ sse_kwargs: dict[str, Any] = {
114
+ "url": f"{self._base_url}/sse",
115
+ "timeout": self._timeout,
116
+ }
117
+ if self._auth is not None:
118
+ sse_kwargs["auth"] = self._auth
119
+ else:
120
+ sse_kwargs["headers"] = self._headers()
121
+ streams = await self._exit_stack.enter_async_context(sse_client(**sse_kwargs))
101
122
  self._session = await self._exit_stack.enter_async_context(
102
123
  ClientSession(*streams)
103
124
  )
@@ -131,11 +152,15 @@ class RemoteClient:
131
152
  from mcp.client.session import ClientSession
132
153
  from mcp.client.sse import sse_client
133
154
 
134
- async with sse_client(
135
- url=f"{self._base_url}/sse",
136
- headers=self._headers(),
137
- timeout=self._timeout,
138
- ) as streams:
155
+ one_shot_kwargs: dict[str, Any] = {
156
+ "url": f"{self._base_url}/sse",
157
+ "timeout": self._timeout,
158
+ }
159
+ if self._auth is not None:
160
+ one_shot_kwargs["auth"] = self._auth
161
+ else:
162
+ one_shot_kwargs["headers"] = self._headers()
163
+ async with sse_client(**one_shot_kwargs) as streams:
139
164
  async with ClientSession(*streams) as session:
140
165
  await session.initialize()
141
166
  result = await session.call_tool(name, arguments)