meta-data-mcp 2.3.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 (128) hide show
  1. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/PKG-INFO +2 -2
  2. meta_data_mcp-2.3.2/meta_data_mcp/__init__.py +1 -0
  3. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/oauth_provider.py +22 -1
  4. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/sdk/remote.py +13 -0
  5. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/server.py +119 -16
  6. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_news_tone_v1.html +9 -4
  7. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/pyproject.toml +1 -1
  8. meta_data_mcp-2.3.0/meta_data_mcp/__init__.py +0 -1
  9. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/.gitignore +0 -0
  10. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/AUTHORS +0 -0
  11. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/LICENSE +0 -0
  12. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/README.md +0 -0
  13. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/cli.py +0 -0
  14. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/client.py +0 -0
  15. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/discovery/__init__.py +0 -0
  16. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/discovery/loader.py +0 -0
  17. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/discovery/state.py +0 -0
  18. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/errors.py +0 -0
  19. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/fields.py +0 -0
  20. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/health.py +0 -0
  21. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/provenance.py +0 -0
  22. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/provider_config.py +0 -0
  23. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/__init__.py +0 -0
  24. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/__template__.py +0 -0
  25. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/au_data_gov.py +0 -0
  26. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/ca_open_gov.py +0 -0
  27. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/cern_opendata.py +0 -0
  28. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/ch_sbb.py +0 -0
  29. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/de_db.py +0 -0
  30. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/eu_copernicus.py +0 -0
  31. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/eu_ecb.py +0 -0
  32. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/eu_eurostat.py +0 -0
  33. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/fr_data_gouv.py +0 -0
  34. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_arxiv.py +0 -0
  35. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_bgpview.py +0 -0
  36. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_coingecko.py +0 -0
  37. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_crossref.py +0 -0
  38. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_dbnomics.py +0 -0
  39. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_disease_sh.py +0 -0
  40. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_epss.py +0 -0
  41. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_europepmc.py +0 -0
  42. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_faostat.py +0 -0
  43. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_frankfurter.py +0 -0
  44. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_gbif.py +0 -0
  45. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_gdelt.py +0 -0
  46. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_imf.py +0 -0
  47. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_inaturalist.py +0 -0
  48. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_met_museum.py +0 -0
  49. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_nvd_cve.py +0 -0
  50. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_oecd.py +0 -0
  51. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_open_library.py +0 -0
  52. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_open_meteo.py +0 -0
  53. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_openalex.py +0 -0
  54. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_openaq.py +0 -0
  55. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_opensanctions.py +0 -0
  56. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_opensky.py +0 -0
  57. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_osm_nominatim.py +0 -0
  58. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_osv_dev.py +0 -0
  59. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_overpass.py +0 -0
  60. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_pubchem.py +0 -0
  61. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_rcsb_pdb.py +0 -0
  62. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_ripe_stat.py +0 -0
  63. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_un_comtrade.py +0 -0
  64. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_unesco_heritage.py +0 -0
  65. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_who_gho.py +0 -0
  66. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_wikidata.py +0 -0
  67. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_wikipedia.py +0 -0
  68. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/global_world_bank.py +0 -0
  69. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/meta_data_mcp.py +0 -0
  70. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/nl_cbs.py +0 -0
  71. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/nl_ndov.py +0 -0
  72. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/nl_rechtspraak.py +0 -0
  73. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/nl_tweedekamer.py +0 -0
  74. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/sg_data_gov.py +0 -0
  75. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/uk_gov.py +0 -0
  76. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/uk_legislation.py +0 -0
  77. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/uk_ons.py +0 -0
  78. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_arcgis_item.py +0 -0
  79. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_cary.py +0 -0
  80. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_cdc_socrata.py +0 -0
  81. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_census_geocoder.py +0 -0
  82. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_cisa_kev.py +0 -0
  83. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_clinicaltrials.py +0 -0
  84. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_courtlistener.py +0 -0
  85. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_data_gov.py +0 -0
  86. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_faa_nasstatus.py +0 -0
  87. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_fayetteville.py +0 -0
  88. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_fda_openfda.py +0 -0
  89. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_federal_register.py +0 -0
  90. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_healthdata_gov.py +0 -0
  91. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_nasa.py +0 -0
  92. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_nc_onemap.py +0 -0
  93. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_ncdeq_gis.py +0 -0
  94. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_noaa_awc.py +0 -0
  95. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_noaa_ncei.py +0 -0
  96. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_noaa_tides.py +0 -0
  97. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_raleigh.py +0 -0
  98. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_sec_edgar.py +0 -0
  99. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_treasury_fiscal.py +0 -0
  100. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/providers/us_usgs_earthquake.py +0 -0
  101. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/registry.py +0 -0
  102. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/routing.py +0 -0
  103. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/sdk/__init__.py +0 -0
  104. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/serialize.py +0 -0
  105. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/transport.py +0 -0
  106. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/__init__.py +0 -0
  107. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_discovery_v1.html +0 -0
  108. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_discovery_v1.py +0 -0
  109. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_entity_graph_v1.html +0 -0
  110. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_entity_graph_v1.py +0 -0
  111. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_molecular_v1.html +0 -0
  112. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_molecular_v1.py +0 -0
  113. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_museum_v1.html +0 -0
  114. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_museum_v1.py +0 -0
  115. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_network_topology_v1.html +0 -0
  116. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_network_topology_v1.py +0 -0
  117. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_news_tone_v1.py +0 -0
  118. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_trade_flows_v1.html +0 -0
  119. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_trade_flows_v1.py +0 -0
  120. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_vulnerability_v1.html +0 -0
  121. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/app_vulnerability_v1.py +0 -0
  122. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/shape_geofeatures_v1.html +0 -0
  123. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/shape_geofeatures_v1.py +0 -0
  124. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/shape_records_v1.html +0 -0
  125. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/shape_records_v1.py +0 -0
  126. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/shape_timeseries_v1.html +0 -0
  127. {meta_data_mcp-2.3.0 → meta_data_mcp-2.3.2}/meta_data_mcp/ui_resources/shape_timeseries_v1.py +0 -0
  128. {meta_data_mcp-2.3.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.3.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
@@ -0,0 +1 @@
1
+ __version__ = "2.3.2"
@@ -87,6 +87,22 @@ class InMemoryOAuthProvider(
87
87
  }
88
88
  return f"{self.issuer_url}/oauth/consent?session={session_token}"
89
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
+
90
106
  def consume_session(self, session_token: str) -> dict[str, Any] | None:
91
107
  """Retrieve and remove a pending consent session (one-shot)."""
92
108
  session = self._auth_sessions.pop(session_token, None)
@@ -136,7 +152,12 @@ class InMemoryOAuthProvider(
136
152
  client: OAuthClientInformationFull,
137
153
  authorization_code: AuthorizationCode,
138
154
  ) -> OAuthToken:
139
- """Validate PKCE, consume the code, and issue access + refresh tokens."""
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
+ """
140
161
  # Remove the used code (one-shot).
141
162
  self._auth_codes.pop(authorization_code.code, None)
142
163
 
@@ -75,6 +75,19 @@ class RemoteClient:
75
75
  timeout: float = 30.0,
76
76
  auth: Any = None,
77
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
+ """
78
91
  if token is not None and auth is not None:
79
92
  raise ValueError(
80
93
  "token and auth are mutually exclusive — use one or the other"
@@ -390,7 +390,18 @@ async def run_server(
390
390
 
391
391
  oauth_issuer = os.getenv("META_DATA_MCP_OAUTH_ISSUER")
392
392
  if oauth_issuer:
393
- from mcp.server.auth.routes import create_auth_routes
393
+ try:
394
+ from mcp.server.auth.routes import (
395
+ create_auth_routes,
396
+ create_protected_resource_routes,
397
+ )
398
+ except ImportError as exc:
399
+ raise RuntimeError(
400
+ "OAuth support requires an MCP SDK version that provides "
401
+ "`mcp.server.auth.routes.create_protected_resource_routes`. "
402
+ "Please upgrade the `mcp` package to a compatible version or "
403
+ "unset META_DATA_MCP_OAUTH_ISSUER to disable OAuth."
404
+ ) from exc
394
405
  from mcp.server.auth.settings import ClientRegistrationOptions
395
406
  from pydantic import AnyHttpUrl
396
407
  from starlette.responses import HTMLResponse
@@ -400,16 +411,46 @@ async def run_server(
400
411
 
401
412
  oauth_provider = InMemoryOAuthProvider(issuer_url=oauth_issuer)
402
413
 
414
+ import html as _html
415
+ from urllib.parse import urlencode, urlsplit, urlunsplit, parse_qsl
416
+
417
+ def _add_query_params(base_url: str, params: dict[str, str]) -> str:
418
+ """Append params to base_url, preserving any existing query string.
419
+
420
+ Uses list-of-tuples rather than a dict so that existing duplicate
421
+ or blank-value query params are never silently dropped. Only the
422
+ keys explicitly provided in ``params`` are added/overwritten.
423
+ """
424
+ parts = urlsplit(base_url)
425
+ # Keep all existing params except any that we're explicitly setting,
426
+ # then append the new ones. This preserves duplicate keys and blanks.
427
+ existing = [
428
+ (k, v)
429
+ for k, v in parse_qsl(parts.query, keep_blank_values=True)
430
+ if k not in params
431
+ ]
432
+ merged = existing + list(params.items())
433
+ return urlunsplit(parts._replace(query=urlencode(merged)))
434
+
403
435
  # Consent page — GET /oauth/consent?session=<token>
404
436
  async def consent_get(request: Request) -> HTMLResponse:
405
437
  session_token = request.query_params.get("session", "")
406
- session = oauth_provider._auth_sessions.get(session_token)
438
+ # Use the provider's own peek method so expiry is enforced
439
+ # without consuming the session (POST /approve does that).
440
+ session = oauth_provider.peek_session(session_token)
407
441
  if session is None:
408
442
  return HTMLResponse(
409
443
  "<h1>Session expired or invalid.</h1>", status_code=400
410
444
  )
411
- client_name = session.get("client_name", session.get("client_id", "?"))
412
- scopes_html = ", ".join(session.get("scopes", [])) or "(default)"
445
+ # HTML-escape all values derived from dynamic client registration
446
+ # to prevent XSS via a maliciously crafted client_name or scope.
447
+ client_name = _html.escape(
448
+ str(session.get("client_name", session.get("client_id", "?")))
449
+ )
450
+ scopes_html = _html.escape(
451
+ ", ".join(session.get("scopes", [])) or "(default)"
452
+ )
453
+ session_token_escaped = _html.escape(session_token)
413
454
  return HTMLResponse(f"""<!DOCTYPE html>
414
455
  <html lang="en"><head><meta charset="utf-8">
415
456
  <title>Authorize — meta-data-mcp</title>
@@ -424,7 +465,7 @@ async def run_server(
424
465
  <p><strong>{client_name}</strong> is requesting access to your meta-data-mcp server.</p>
425
466
  <p class="scope">Requested scopes: {scopes_html}</p>
426
467
  <form method="POST" action="/oauth/consent/approve">
427
- <input type="hidden" name="session" value="{session_token}">
468
+ <input type="hidden" name="session" value="{session_token_escaped}">
428
469
  <button type="submit" class="approve">Approve</button>
429
470
  <button type="submit" name="deny" value="1" class="deny">Deny</button>
430
471
  </form>
@@ -433,7 +474,6 @@ async def run_server(
433
474
  # Consent approval — POST /oauth/consent/approve
434
475
  async def consent_post(request: Request) -> HTMLResponse:
435
476
  from starlette.responses import RedirectResponse
436
- from urllib.parse import urlencode
437
477
 
438
478
  form = await request.form()
439
479
  session_token = str(form.get("session", ""))
@@ -444,35 +484,98 @@ async def run_server(
444
484
  )
445
485
  if form.get("deny"):
446
486
  redirect_uri = session["redirect_uri"]
447
- params = {"error": "access_denied"}
487
+ params: dict[str, str] = {"error": "access_denied"}
448
488
  if session.get("state"):
449
489
  params["state"] = session["state"]
450
490
  return RedirectResponse(
451
- f"{redirect_uri}?{urlencode(params)}", status_code=302
491
+ _add_query_params(redirect_uri, params), status_code=302
452
492
  )
453
493
  code = oauth_provider.create_authorization_code(session)
454
494
  params = {"code": code}
455
495
  if session.get("state"):
456
496
  params["state"] = session["state"]
457
497
  return RedirectResponse(
458
- f"{session['redirect_uri']}?{urlencode(params)}", status_code=302
498
+ _add_query_params(session["redirect_uri"], params), status_code=302
499
+ )
500
+
501
+ configured_resource_public_url = (
502
+ os.getenv("META_DATA_MCP_PUBLIC_URL", "").strip() or None
503
+ )
504
+ resource_public_url = configured_resource_public_url or oauth_issuer
505
+ validated_resource_url = AnyHttpUrl(resource_public_url)
506
+ validated_oauth_issuer = AnyHttpUrl(oauth_issuer)
507
+
508
+ if configured_resource_public_url is None:
509
+ log.info(
510
+ "META_DATA_MCP_PUBLIC_URL is not set; defaulting protected "
511
+ "resource URL to META_DATA_MCP_OAUTH_ISSUER (%s)",
512
+ oauth_issuer,
513
+ )
514
+ elif hmac.compare_digest(
515
+ resource_public_url.rstrip("/"), oauth_issuer.rstrip("/")
516
+ ):
517
+ log.info(
518
+ "META_DATA_MCP_PUBLIC_URL matches META_DATA_MCP_OAUTH_ISSUER; "
519
+ "using %s for both protected resource URL and issuer",
520
+ resource_public_url,
521
+ )
522
+ else:
523
+ log.warning(
524
+ "META_DATA_MCP_PUBLIC_URL (%s) differs from "
525
+ "META_DATA_MCP_OAUTH_ISSUER (%s); using "
526
+ "META_DATA_MCP_PUBLIC_URL for the protected resource URL "
527
+ "and META_DATA_MCP_OAUTH_ISSUER for the authorization server "
528
+ "issuer",
529
+ resource_public_url,
530
+ oauth_issuer,
459
531
  )
460
532
 
533
+ # Serve both RFC 9728 variants:
534
+ # - Base: /.well-known/oauth-protected-resource
535
+ # - Path: /.well-known/oauth-protected-resource/sse
536
+ # Clients (e.g. Claude Desktop) probe both.
537
+ protected_resource_routes = create_protected_resource_routes(
538
+ resource_url=validated_resource_url,
539
+ authorization_servers=[validated_oauth_issuer],
540
+ scopes_supported=["opendata"],
541
+ ) + create_protected_resource_routes(
542
+ resource_url=AnyHttpUrl(resource_public_url.rstrip("/") + "/sse"),
543
+ authorization_servers=[validated_oauth_issuer],
544
+ scopes_supported=["opendata"],
545
+ )
461
546
  oauth_routes = create_auth_routes(
462
547
  provider=oauth_provider,
463
- issuer_url=AnyHttpUrl(oauth_issuer),
548
+ issuer_url=validated_oauth_issuer,
464
549
  client_registration_options=ClientRegistrationOptions(
465
550
  enabled=True,
466
551
  valid_scopes=["opendata"],
467
552
  default_scopes=["opendata"],
468
553
  ),
469
554
  )
470
- extra_routes = oauth_routes + [
471
- Route("/oauth/consent", endpoint=consent_get, methods=["GET"]),
472
- Route(
473
- "/oauth/consent/approve", endpoint=consent_post, methods=["POST"]
474
- ),
475
- ]
555
+ from starlette.responses import RedirectResponse as _RedirectResponse
556
+
557
+ async def oidc_discovery(_request):
558
+ return _RedirectResponse(
559
+ "/.well-known/oauth-authorization-server", status_code=301
560
+ )
561
+
562
+ extra_routes = (
563
+ protected_resource_routes
564
+ + oauth_routes
565
+ + [
566
+ Route(
567
+ "/.well-known/openid-configuration",
568
+ endpoint=oidc_discovery,
569
+ methods=["GET"],
570
+ ),
571
+ Route("/oauth/consent", endpoint=consent_get, methods=["GET"]),
572
+ Route(
573
+ "/oauth/consent/approve",
574
+ endpoint=consent_post,
575
+ methods=["POST"],
576
+ ),
577
+ ]
578
+ )
476
579
  log.info(f"OAuth 2.0 enabled — issuer: {oauth_issuer}")
477
580
 
478
581
  app = Starlette(
@@ -374,6 +374,10 @@
374
374
  return m ? m[1] : '';
375
375
  }
376
376
 
377
+ function safeUrl(url) {
378
+ return url && /^https?:\/\//i.test(url) ? url : null;
379
+ }
380
+
377
381
  function sortByDate(arr) {
378
382
  return arr.slice().sort(function (a, b) {
379
383
  return a.date < b.date ? -1 : a.date > b.date ? 1 : 0;
@@ -550,9 +554,10 @@
550
554
  row('Domain', ev.domain || '-');
551
555
  row('Language', ev.language || '-');
552
556
  card.appendChild(dl);
553
- if (ev.url) {
557
+ var safe = safeUrl(ev.url);
558
+ if (safe) {
554
559
  card.appendChild(el('a', {
555
- href: ev.url, target: '_blank', rel: 'noopener noreferrer',
560
+ href: safe, target: '_blank', rel: 'noopener noreferrer',
556
561
  text: ev.url,
557
562
  }));
558
563
  }
@@ -665,7 +670,7 @@
665
670
  renderRightPanel();
666
671
  });
667
672
  g.addEventListener('click', function () {
668
- if (ev.url) window.open(ev.url, '_blank', 'noopener,noreferrer');
673
+ var u = safeUrl(ev.url); if (u) window.open(u, '_blank', 'noopener,noreferrer');
669
674
  });
670
675
  var titleEl = svg('title');
671
676
  titleEl.textContent = ev.date + ' - ' + (ev.country || '-') +
@@ -827,7 +832,7 @@
827
832
  renderRightPanel();
828
833
  });
829
834
  tr.addEventListener('click', function () {
830
- if (ev.url) window.open(ev.url, '_blank', 'noopener,noreferrer');
835
+ var u = safeUrl(ev.url); if (u) window.open(u, '_blank', 'noopener,noreferrer');
831
836
  });
832
837
  tbody.appendChild(tr);
833
838
  });
@@ -35,7 +35,7 @@ dependencies = [
35
35
  "anyio>=4.0.0",
36
36
  "click>=8.1.7",
37
37
  "httpx>=0.28.0",
38
- "mcp>=1.0.0",
38
+ "mcp>=1.9.0",
39
39
  "pydantic>=2.0.0",
40
40
  "pyyaml>=6.0",
41
41
  "starlette>=1.0.0",
@@ -1 +0,0 @@
1
- __version__ = "2.3.0"
File without changes
File without changes
File without changes
File without changes