datajunction 0.0.147__tar.gz → 0.0.148__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 (174) hide show
  1. {datajunction-0.0.147 → datajunction-0.0.148}/PKG-INFO +1 -2
  2. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/__about__.py +1 -1
  3. datajunction-0.0.148/datajunction/mcp/__init__.py +12 -0
  4. datajunction-0.0.148/datajunction/mcp/cli.py +135 -0
  5. datajunction-0.0.148/datajunction/mcp/config.py +71 -0
  6. {datajunction-0.0.147 → datajunction-0.0.148}/pyproject.toml +5 -2
  7. datajunction-0.0.148/tests/mcp/README.md +16 -0
  8. datajunction-0.0.148/tests/mcp/test_cli.py +285 -0
  9. datajunction-0.0.147/datajunction/mcp/__init__.py +0 -6
  10. datajunction-0.0.147/datajunction/mcp/cli.py +0 -52
  11. datajunction-0.0.147/datajunction/mcp/config.py +0 -27
  12. datajunction-0.0.147/datajunction/mcp/formatters.py +0 -289
  13. datajunction-0.0.147/datajunction/mcp/server.py +0 -554
  14. datajunction-0.0.147/datajunction/mcp/tools.py +0 -1492
  15. datajunction-0.0.147/tests/mcp/README.md +0 -99
  16. datajunction-0.0.147/tests/mcp/test_cli.py +0 -102
  17. datajunction-0.0.147/tests/mcp/test_formatters.py +0 -454
  18. datajunction-0.0.147/tests/mcp/test_server_tools.py +0 -585
  19. datajunction-0.0.147/tests/mcp/test_tools.py +0 -3119
  20. datajunction-0.0.147/tests/mcp/test_visualize_metrics.py +0 -1491
  21. {datajunction-0.0.147 → datajunction-0.0.148}/.coveragerc +0 -0
  22. {datajunction-0.0.147 → datajunction-0.0.148}/.gitignore +0 -0
  23. {datajunction-0.0.147 → datajunction-0.0.148}/.pre-commit-config.yaml +0 -0
  24. {datajunction-0.0.147 → datajunction-0.0.148}/LICENSE.txt +0 -0
  25. {datajunction-0.0.147 → datajunction-0.0.148}/Makefile +0 -0
  26. {datajunction-0.0.147 → datajunction-0.0.148}/README.md +0 -0
  27. {datajunction-0.0.147 → datajunction-0.0.148}/claude_desktop_config.example.json +0 -0
  28. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/__init__.py +0 -0
  29. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/_base.py +0 -0
  30. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/_internal.py +0 -0
  31. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/admin.py +0 -0
  32. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/builder.py +0 -0
  33. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/cli.py +0 -0
  34. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/client.py +0 -0
  35. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/compile.py +0 -0
  36. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/deployment.py +0 -0
  37. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/exceptions.py +0 -0
  38. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/models.py +0 -0
  39. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/nodes.py +0 -0
  40. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/rendering.py +0 -0
  41. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/seed/init_system_nodes.py +0 -0
  42. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/seed/nodes/date.dimension.yaml +0 -0
  43. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/seed/nodes/dimension_link.dimension.yaml +0 -0
  44. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/seed/nodes/dj.yaml +0 -0
  45. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/seed/nodes/is_active.dimension.yaml +0 -0
  46. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/seed/nodes/materialization.dimension.yaml +0 -0
  47. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/seed/nodes/node_type.dimension.yaml +0 -0
  48. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/seed/nodes/node_without_description.metric.yaml +0 -0
  49. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/seed/nodes/nodes.dimension.yaml +0 -0
  50. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/seed/nodes/number_of_materializations.metric.yaml +0 -0
  51. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/seed/nodes/number_of_nodes.metric.yaml +0 -0
  52. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/seed/nodes/user.dimension.yaml +0 -0
  53. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/skills/datajunction.md +0 -0
  54. {datajunction-0.0.147 → datajunction-0.0.148}/datajunction/tags.py +0 -0
  55. {datajunction-0.0.147 → datajunction-0.0.148}/setup.cfg +0 -0
  56. {datajunction-0.0.147 → datajunction-0.0.148}/tests/__init__.py +0 -0
  57. {datajunction-0.0.147 → datajunction-0.0.148}/tests/conftest.py +0 -0
  58. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/deploy0/dj.yaml +0 -0
  59. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/deploy0/roads/companies.yaml +0 -0
  60. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/deploy0/roads/companies_dim.yaml +0 -0
  61. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/deploy0/roads/contractor.yaml +0 -0
  62. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/deploy0/roads/contractors.yaml +0 -0
  63. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/deploy0/roads/us_state.yaml +0 -0
  64. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/deploy0/roads/us_states.yaml +0 -0
  65. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/dj.yaml +0 -0
  66. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/avg_length_of_employment.metric.yaml +0 -0
  67. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/avg_repair_price.metric.yaml +0 -0
  68. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/avg_time_to_dispatch.metric.yaml +0 -0
  69. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/contractor.dimension.yaml +0 -0
  70. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/contractors.source.yaml +0 -0
  71. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/date.source.yaml +0 -0
  72. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/date_dim.dimension.yaml +0 -0
  73. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/dispatcher.dimension.yaml +0 -0
  74. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/dispatchers.source.yaml +0 -0
  75. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/hard_hat.dimension.yaml +0 -0
  76. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/hard_hat_state.source.yaml +0 -0
  77. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/hard_hats.source.yaml +0 -0
  78. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/local_hard_hats.dimension.yaml +0 -0
  79. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/municipality.source.yaml +0 -0
  80. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/municipality_dim.dimension.yaml +0 -0
  81. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/municipality_municipality_type.source.yaml +0 -0
  82. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/municipality_type.source.yaml +0 -0
  83. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/national_level_agg.transform.yaml +0 -0
  84. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/num_repair_orders.metric.yaml +0 -0
  85. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/regional_level_agg.transform.yaml +0 -0
  86. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/regional_repair_efficiency.metric.yaml +0 -0
  87. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/repair_order.dimension.yaml +0 -0
  88. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/repair_order_details.source.yaml +0 -0
  89. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/repair_order_transform.transform.yaml +0 -0
  90. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/repair_orders.source.yaml +0 -0
  91. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/repair_orders_cube.cube.yaml +0 -0
  92. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/repair_type.source.yaml +0 -0
  93. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/total_repair_cost.metric.yaml +0 -0
  94. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/total_repair_order_discounts.metric.yaml +0 -0
  95. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/us_region.source.yaml +0 -0
  96. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/us_state.dimension.yaml +0 -0
  97. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project1/roads/us_states.source.yaml +0 -0
  98. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project10/dj.yaml +0 -0
  99. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/avg_length_of_employment.metric.yaml +0 -0
  100. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/avg_repair_price.metric.yaml +0 -0
  101. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/avg_time_to_dispatch.metric.yaml +0 -0
  102. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/contractor.dimension.yaml +0 -0
  103. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/contractors.source.yaml +0 -0
  104. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/date.source.yaml +0 -0
  105. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/date_dim.dimension.yaml +0 -0
  106. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/dispatcher.dimension.yaml +0 -0
  107. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/dispatchers.source.yaml +0 -0
  108. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/dj.yaml +0 -0
  109. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/hard_hat.dimension.yaml +0 -0
  110. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/hard_hat_state.source.yaml +0 -0
  111. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/hard_hats.source.yaml +0 -0
  112. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/local_hard_hats.dimension.yaml +0 -0
  113. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/municipality.source.yaml +0 -0
  114. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/municipality_dim.dimension.yaml +0 -0
  115. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/municipality_municipality_type.source.yaml +0 -0
  116. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/municipality_type.source.yaml +0 -0
  117. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/national_level_agg.transform.yaml +0 -0
  118. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/num_repair_orders.metric.yaml +0 -0
  119. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/regional_level_agg.transform.yaml +0 -0
  120. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/regional_repair_efficiency.metric.yaml +0 -0
  121. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/repair_order.dimension.yaml +0 -0
  122. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/repair_order_details.source.yaml +0 -0
  123. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/repair_order_transform.transform.yaml +0 -0
  124. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/repair_orders.source.yaml +0 -0
  125. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/repair_orders_cube.cube.yaml +0 -0
  126. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/repair_type.source.yaml +0 -0
  127. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/total_repair_cost.metric.yaml +0 -0
  128. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/total_repair_order_discounts.metric.yaml +0 -0
  129. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/us_region.source.yaml +0 -0
  130. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/us_state.dimension.yaml +0 -0
  131. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project11/us_states.source.yaml +0 -0
  132. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project12/dj.yaml +0 -0
  133. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project12/roads/companies.source.yaml +0 -0
  134. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project12/roads/companies_dim.dimension.yaml +0 -0
  135. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project12/roads/contractor.dimension.yaml +0 -0
  136. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project12/roads/contractors.source.yaml +0 -0
  137. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project12/roads/us_state.dimension.yaml +0 -0
  138. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project12/roads/us_states.source.yaml +0 -0
  139. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project2/dj.yaml +0 -0
  140. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project2/some_node.source.yaml +0 -0
  141. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project3/dj.yaml +0 -0
  142. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project3/some_node.yaml +0 -0
  143. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project4/dj.yaml +0 -0
  144. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project4/very/very/deeply/nested/namespace/some_node.source.yaml +0 -0
  145. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project5/dj.yaml +0 -0
  146. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project5/some_node.a.b.c.source.yaml +0 -0
  147. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project6/dj.yaml +0 -0
  148. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project6/roads/contractor.dimension.yaml +0 -0
  149. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project6/roads/contractors.source.yaml +0 -0
  150. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project7/dj.yaml +0 -0
  151. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project7/roads/contractor.dimension.yaml +0 -0
  152. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project7/roads/contractors.source.yaml +0 -0
  153. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project8/dj.yaml +0 -0
  154. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project9/dj.yaml +0 -0
  155. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project9/roads/companies.source.yaml +0 -0
  156. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project9/roads/companies_dim.dimension.yaml +0 -0
  157. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project9/roads/contractor.dimension.yaml +0 -0
  158. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project9/roads/contractors.source.yaml +0 -0
  159. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project9/roads/us_state.dimension.yaml +0 -0
  160. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples/project9/roads/us_states.source.yaml +0 -0
  161. {datajunction-0.0.147 → datajunction-0.0.148}/tests/examples.py +0 -0
  162. {datajunction-0.0.147 → datajunction-0.0.148}/tests/mcp/__init__.py +0 -0
  163. {datajunction-0.0.147 → datajunction-0.0.148}/tests/test__internal.py +0 -0
  164. {datajunction-0.0.147 → datajunction-0.0.148}/tests/test_admin.py +0 -0
  165. {datajunction-0.0.147 → datajunction-0.0.148}/tests/test_base.py +0 -0
  166. {datajunction-0.0.147 → datajunction-0.0.148}/tests/test_builder.py +0 -0
  167. {datajunction-0.0.147 → datajunction-0.0.148}/tests/test_cli.py +0 -0
  168. {datajunction-0.0.147 → datajunction-0.0.148}/tests/test_client.py +0 -0
  169. {datajunction-0.0.147 → datajunction-0.0.148}/tests/test_compile.py +0 -0
  170. {datajunction-0.0.147 → datajunction-0.0.148}/tests/test_deploy.py +0 -0
  171. {datajunction-0.0.147 → datajunction-0.0.148}/tests/test_generated_client.py +0 -0
  172. {datajunction-0.0.147 → datajunction-0.0.148}/tests/test_integration.py +0 -0
  173. {datajunction-0.0.147 → datajunction-0.0.148}/tests/test_models.py +0 -0
  174. {datajunction-0.0.147 → datajunction-0.0.148}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datajunction
3
- Version: 0.0.147
3
+ Version: 0.0.148
4
4
  Summary: DataJunction client library for connecting to a DataJunction server
5
5
  Project-URL: repository, https://github.com/DataJunction/dj
6
6
  Author-email: DataJunction Authors <yian.shang@gmail.com>
@@ -20,7 +20,6 @@ Requires-Dist: requests<3.0.0,>=2.28.2
20
20
  Requires-Dist: rich>=13.7.0
21
21
  Provides-Extra: mcp
22
22
  Requires-Dist: mcp>=1.0.0; extra == 'mcp'
23
- Requires-Dist: plotext>=5.2.8; extra == 'mcp'
24
23
  Requires-Dist: pydantic-settings>=2.10.1; extra == 'mcp'
25
24
  Requires-Dist: pydantic>=2.0; extra == 'mcp'
26
25
  Provides-Extra: pandas
@@ -2,4 +2,4 @@
2
2
  Version for Hatch
3
3
  """
4
4
 
5
- __version__ = "0.0.147"
5
+ __version__ = "0.0.148"
@@ -0,0 +1,12 @@
1
+ """
2
+ DataJunction MCP stdio→HTTP proxy.
3
+
4
+ The canonical MCP server is hosted by ``datajunction-server`` at the
5
+ ``/mcp`` endpoint (Streamable HTTP transport). This package provides a
6
+ small ``dj-mcp`` CLI that bridges stdio MCP clients (e.g. Claude Desktop)
7
+ to the hosted server, so users on stdio-only clients can still talk to a
8
+ deployed DJ.
9
+
10
+ For HTTP-capable clients (Claude Code, Cursor, Slack agents) connect to
11
+ the hosted ``/mcp`` endpoint directly — no local CLI required.
12
+ """
@@ -0,0 +1,135 @@
1
+ """
2
+ Stdio→HTTP proxy for the DataJunction MCP server.
3
+
4
+ The server-side MCP implementation now lives in ``datajunction-server``
5
+ (`/mcp` endpoint, Streamable HTTP transport). This CLI exists so users on
6
+ clients that only speak stdio (e.g. Claude Desktop today) can still talk
7
+ to a hosted DJ MCP without running anything heavyweight locally.
8
+
9
+ It opens an HTTP MCP client to the configured hosted endpoint, hosts a
10
+ stdio MCP server, and forwards every list_tools / call_tool / list_resources /
11
+ read_resource request to the upstream. The wire shape is identical, so
12
+ clients see the same tool surface as the hosted server exposes.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import logging
19
+ import sys
20
+
21
+ import mcp.types as types
22
+ from mcp.client.session import ClientSession
23
+ from mcp.client.streamable_http import streamablehttp_client
24
+ from mcp.server import Server
25
+ from mcp.server.stdio import stdio_server
26
+
27
+ from datajunction.mcp.config import get_mcp_settings
28
+
29
+ logging.basicConfig(
30
+ level=logging.INFO,
31
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
32
+ stream=sys.stderr, # stdout is reserved for MCP protocol traffic
33
+ )
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ async def _proxy_list_tools(upstream: ClientSession) -> list[types.Tool]:
38
+ """Forward ``tools/list`` to the upstream session."""
39
+ result = await upstream.list_tools()
40
+ return list(result.tools)
41
+
42
+
43
+ async def _proxy_call_tool(
44
+ upstream: ClientSession,
45
+ name: str,
46
+ arguments: dict,
47
+ ) -> list[types.ContentBlock]:
48
+ """Forward ``tools/call`` to the upstream session.
49
+
50
+ ``result.content`` may be a tuple/list of content blocks; normalize to list.
51
+ """
52
+ result = await upstream.call_tool(name, arguments=arguments or {})
53
+ return list(result.content)
54
+
55
+
56
+ async def _proxy_list_resources(upstream: ClientSession) -> list[types.Resource]:
57
+ """Forward ``resources/list`` to the upstream session."""
58
+ result = await upstream.list_resources()
59
+ return list(result.resources)
60
+
61
+
62
+ async def _proxy_read_resource(upstream: ClientSession, uri: str) -> str:
63
+ """Forward ``resources/read`` and surface the first text payload.
64
+
65
+ The hosted DJ doesn't expose any resources today, but if it ever does
66
+ we return the first ``text`` content block.
67
+ """
68
+ result = await upstream.read_resource(uri) # type: ignore[arg-type]
69
+ for block in result.contents:
70
+ text = getattr(block, "text", None)
71
+ if text is not None:
72
+ return text
73
+ return "" # pragma: no cover
74
+
75
+
76
+ def _build_proxy_app(upstream: ClientSession) -> Server:
77
+ """Create an MCP ``Server`` whose handlers all forward to ``upstream``.
78
+
79
+ Split out from ``_serve`` so the wiring is testable without spinning
80
+ up stdio.
81
+ """
82
+ app: Server = Server("datajunction")
83
+ app.list_tools()(lambda: _proxy_list_tools(upstream))
84
+ app.call_tool()(
85
+ lambda name, arguments: _proxy_call_tool(upstream, name, arguments),
86
+ )
87
+ app.list_resources()(lambda: _proxy_list_resources(upstream))
88
+ app.read_resource()(lambda uri: _proxy_read_resource(upstream, uri))
89
+ return app
90
+
91
+
92
+ async def _serve(upstream: ClientSession) -> None:
93
+ """Run a stdio MCP server that forwards every request to ``upstream``."""
94
+ app = _build_proxy_app(upstream)
95
+ async with stdio_server() as (read_stream, write_stream):
96
+ await app.run(
97
+ read_stream,
98
+ write_stream,
99
+ app.create_initialization_options(),
100
+ )
101
+
102
+
103
+ async def main() -> None:
104
+ """Connect to the hosted DJ MCP and bridge it to stdio."""
105
+ settings = get_mcp_settings()
106
+
107
+ headers = {}
108
+ if settings.dj_api_token:
109
+ headers["Authorization"] = f"Bearer {settings.dj_api_token}"
110
+
111
+ logger.info("Bridging stdio MCP → %s", settings.mcp_url)
112
+
113
+ async with streamablehttp_client(
114
+ url=settings.mcp_url,
115
+ headers=headers or None,
116
+ timeout=settings.request_timeout,
117
+ ) as (read, write, _get_session_id):
118
+ async with ClientSession(read, write) as upstream:
119
+ await upstream.initialize()
120
+ await _serve(upstream)
121
+
122
+
123
+ def run() -> None:
124
+ """Synchronous entrypoint for ``dj-mcp`` script registration."""
125
+ try:
126
+ asyncio.run(main())
127
+ except KeyboardInterrupt:
128
+ logger.info("dj-mcp stopped by user")
129
+ except Exception as exc:
130
+ logger.error("dj-mcp error: %s", exc, exc_info=True)
131
+ sys.exit(1)
132
+
133
+
134
+ if __name__ == "__main__":
135
+ run()
@@ -0,0 +1,71 @@
1
+ """
2
+ Configuration for the DataJunction MCP stdio→HTTP proxy CLI.
3
+
4
+ The CLI connects to a hosted DJ ``/mcp`` endpoint over Streamable HTTP and
5
+ re-exposes its tools to local stdio MCP clients (e.g. Claude Desktop).
6
+
7
+ Settings are read from env on every ``get_mcp_settings()`` call (no caching),
8
+ so override behavior is predictable in tests and shell-set values are picked
9
+ up at startup.
10
+
11
+ Resolution order for the upstream MCP URL:
12
+
13
+ 1. ``DJ_MCP_URL`` if set — full URL of the ``/mcp`` endpoint.
14
+ 2. ``DJ_API_URL`` if set — backwards-compatible fallback for users of the
15
+ pre-migration ``dj-mcp`` CLI. We append ``/mcp`` to it.
16
+ 3. ``http://localhost:8000/mcp`` — local-dev default.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ from typing import Optional
23
+
24
+ from pydantic import Field
25
+ from pydantic_settings import BaseSettings, SettingsConfigDict
26
+
27
+
28
+ def _resolve_default_mcp_url() -> str:
29
+ """Fall back to the legacy ``DJ_API_URL`` env var when ``DJ_MCP_URL``
30
+ is unset.
31
+
32
+ Pre-migration deployments configured the stdio CLI with ``DJ_API_URL``
33
+ (the base DJ API URL); the new CLI talks directly to ``/mcp``. We read
34
+ ``DJ_API_URL`` and append the path so existing setups don't silently
35
+ break.
36
+
37
+ Pydantic-settings reads ``DJ_MCP_URL`` via the field alias before this
38
+ factory fires, so we only need to handle the legacy + default cases.
39
+ """
40
+ legacy = os.environ.get("DJ_API_URL")
41
+ if legacy:
42
+ return legacy.rstrip("/") + "/mcp"
43
+ return "http://localhost:8000/mcp"
44
+
45
+
46
+ class MCPSettings(BaseSettings):
47
+ """Settings for the dj-mcp stdio CLI."""
48
+
49
+ # Hosted DJ MCP endpoint. Override with ``DJ_MCP_URL``; ``DJ_API_URL``
50
+ # is accepted as a backwards-compatible fallback (see module docstring).
51
+ mcp_url: str = Field(
52
+ default_factory=_resolve_default_mcp_url,
53
+ alias="DJ_MCP_URL",
54
+ )
55
+
56
+ # Bearer token forwarded as Authorization on every upstream request.
57
+ # Use ``DJ_API_TOKEN`` to align with the rest of the DJ tooling.
58
+ dj_api_token: Optional[str] = Field(default=None, alias="DJ_API_TOKEN")
59
+
60
+ # Request timeout for upstream calls.
61
+ request_timeout: float = 30.0
62
+
63
+ model_config = SettingsConfigDict(
64
+ case_sensitive=False,
65
+ populate_by_name=True,
66
+ )
67
+
68
+
69
+ def get_mcp_settings() -> MCPSettings:
70
+ """Return MCP settings. Re-reads env on every call."""
71
+ return MCPSettings()
@@ -30,11 +30,14 @@ classifiers = [
30
30
 
31
31
  [project.optional-dependencies]
32
32
  pandas = ["pandas>=2.0.2"]
33
+ # Optional dependencies for the dj-mcp stdio→HTTP proxy CLI. plotext was
34
+ # previously needed for client-side rendering of `visualize_metrics`; that
35
+ # logic now lives server-side in datajunction-server, so the client only
36
+ # needs the MCP SDK + pydantic-settings to read env-var configuration.
33
37
  mcp = [
34
38
  "mcp>=1.0.0",
35
39
  "pydantic>=2.0",
36
40
  "pydantic-settings>=2.10.1",
37
- "plotext>=5.2.8",
38
41
  ]
39
42
 
40
43
  [tool.hatch.version]
@@ -90,5 +93,5 @@ test = [
90
93
  "nbformat>=5.10.4",
91
94
  "sqlglot>=18.0.1",
92
95
  "pydantic-settings>=2.11.0",
93
- "mcp>=1.0.0", # For MCP tests
96
+ "mcp>=1.0.0", # For dj-mcp proxy CLI tests
94
97
  ]
@@ -0,0 +1,16 @@
1
+ # dj-mcp CLI tests
2
+
3
+ The DataJunction MCP server is hosted in `datajunction-server` at the
4
+ `/mcp` endpoint (Streamable HTTP transport). The client package only
5
+ ships a thin stdio→HTTP proxy CLI (`dj-mcp`) for stdio-only MCP clients
6
+ like Claude Desktop.
7
+
8
+ These tests cover the proxy wiring:
9
+
10
+ - env vars (`DJ_API_TOKEN`, `DJ_MCP_URL`) → upstream URL + Authorization header
11
+ - `run()` invokes `asyncio.run(main())`
12
+
13
+ End-to-end tool behavior is covered server-side under
14
+ `datajunction-server/tests/dj_mcp/`. There's no per-tool coverage here
15
+ because the CLI doesn't implement tools — it forwards every request to
16
+ the hosted server.
@@ -0,0 +1,285 @@
1
+ """
2
+ Tests for the dj-mcp stdio→HTTP proxy CLI.
3
+
4
+ The CLI delegates everything to a hosted DJ MCP server via Streamable HTTP.
5
+ We just verify the wiring: settings → headers, upstream connect, stdio
6
+ bridge runs. Actual tool behavior is tested in datajunction-server's
7
+ ``tests/dj_mcp/`` suite.
8
+ """
9
+
10
+ from unittest.mock import AsyncMock, MagicMock
11
+
12
+ import mcp.types as types
13
+ import pytest
14
+ from mcp.server import Server
15
+
16
+ from datajunction.mcp import cli
17
+
18
+
19
+ def test_dj_api_url_backwards_compat(monkeypatch) -> None:
20
+ """Pre-migration deployments set ``DJ_API_URL``; the new CLI honours
21
+ it and appends ``/mcp`` so existing setups don't silently break."""
22
+ from datajunction.mcp.config import get_mcp_settings
23
+
24
+ monkeypatch.delenv("DJ_MCP_URL", raising=False)
25
+ monkeypatch.setenv("DJ_API_URL", "https://dj.example.com")
26
+ assert get_mcp_settings().mcp_url == "https://dj.example.com/mcp"
27
+
28
+
29
+ def test_dj_mcp_url_takes_precedence_over_dj_api_url(monkeypatch) -> None:
30
+ """If both are set, ``DJ_MCP_URL`` wins — no surprise when explicitly configured."""
31
+ from datajunction.mcp.config import get_mcp_settings
32
+
33
+ monkeypatch.setenv("DJ_MCP_URL", "https://override/mcp")
34
+ monkeypatch.setenv("DJ_API_URL", "https://legacy")
35
+ assert get_mcp_settings().mcp_url == "https://override/mcp"
36
+
37
+
38
+ def test_default_mcp_url_when_neither_set(monkeypatch) -> None:
39
+ """No env vars → localhost default."""
40
+ from datajunction.mcp.config import get_mcp_settings
41
+
42
+ monkeypatch.delenv("DJ_MCP_URL", raising=False)
43
+ monkeypatch.delenv("DJ_API_URL", raising=False)
44
+ assert get_mcp_settings().mcp_url == "http://localhost:8000/mcp"
45
+
46
+
47
+ @pytest.mark.asyncio
48
+ async def test_main_connects_with_bearer_token_when_set(monkeypatch):
49
+ """When DJ_API_TOKEN is set, it's forwarded as an Authorization header."""
50
+ monkeypatch.setenv("DJ_API_TOKEN", "test-token")
51
+ monkeypatch.setenv("DJ_MCP_URL", "http://example.com/mcp")
52
+
53
+ captured: dict = {}
54
+
55
+ class _FakeStreamCtx:
56
+ async def __aenter__(self):
57
+ return (MagicMock(), MagicMock(), lambda: None)
58
+
59
+ async def __aexit__(self, *exc):
60
+ return False
61
+
62
+ def fake_streamablehttp_client(url, headers=None, timeout=None):
63
+ captured["url"] = url
64
+ captured["headers"] = headers
65
+ return _FakeStreamCtx()
66
+
67
+ class _FakeSessionCtx:
68
+ def __init__(self, *a, **kw):
69
+ pass
70
+
71
+ async def __aenter__(self):
72
+ session = MagicMock()
73
+ session.initialize = AsyncMock()
74
+ return session
75
+
76
+ async def __aexit__(self, *exc):
77
+ return False
78
+
79
+ monkeypatch.setattr(cli, "streamablehttp_client", fake_streamablehttp_client)
80
+ monkeypatch.setattr(cli, "ClientSession", _FakeSessionCtx)
81
+ monkeypatch.setattr(cli, "_serve", AsyncMock())
82
+
83
+ await cli.main()
84
+
85
+ assert captured["url"] == "http://example.com/mcp"
86
+ assert captured["headers"] == {"Authorization": "Bearer test-token"}
87
+
88
+
89
+ @pytest.mark.asyncio
90
+ async def test_main_without_token_omits_auth_header(monkeypatch):
91
+ """No DJ_API_TOKEN → no Authorization header (None passed to httpx)."""
92
+ monkeypatch.delenv("DJ_API_TOKEN", raising=False)
93
+ monkeypatch.setenv("DJ_MCP_URL", "http://example.com/mcp")
94
+
95
+ captured: dict = {}
96
+
97
+ class _FakeStreamCtx:
98
+ async def __aenter__(self):
99
+ return (MagicMock(), MagicMock(), lambda: None)
100
+
101
+ async def __aexit__(self, *exc):
102
+ return False
103
+
104
+ def fake_streamablehttp_client(url, headers=None, timeout=None):
105
+ captured["headers"] = headers
106
+ return _FakeStreamCtx()
107
+
108
+ class _FakeSessionCtx:
109
+ def __init__(self, *a, **kw):
110
+ pass
111
+
112
+ async def __aenter__(self):
113
+ session = MagicMock()
114
+ session.initialize = AsyncMock()
115
+ return session
116
+
117
+ async def __aexit__(self, *exc):
118
+ return False
119
+
120
+ monkeypatch.setattr(cli, "streamablehttp_client", fake_streamablehttp_client)
121
+ monkeypatch.setattr(cli, "ClientSession", _FakeSessionCtx)
122
+ monkeypatch.setattr(cli, "_serve", AsyncMock())
123
+
124
+ await cli.main()
125
+ assert captured["headers"] is None
126
+
127
+
128
+ def test_run_invokes_asyncio_run(monkeypatch):
129
+ """``run()`` is the script entrypoint — it should drive ``main()`` via asyncio."""
130
+
131
+ invoked = {}
132
+
133
+ def fake_asyncio_run(coro):
134
+ invoked["called"] = True
135
+ coro.close() # avoid "coroutine was never awaited" warning
136
+
137
+ monkeypatch.setattr(cli.asyncio, "run", fake_asyncio_run)
138
+ cli.run()
139
+ assert invoked["called"]
140
+
141
+
142
+ def test_run_swallows_keyboard_interrupt(monkeypatch):
143
+ """``Ctrl-C`` in the CLI should log and exit cleanly, not propagate."""
144
+
145
+ def fake_asyncio_run(coro):
146
+ coro.close()
147
+ raise KeyboardInterrupt
148
+
149
+ monkeypatch.setattr(cli.asyncio, "run", fake_asyncio_run)
150
+ cli.run() # must not raise
151
+
152
+
153
+ def test_run_logs_and_exits_on_unhandled_exception(monkeypatch):
154
+ """Any other exception is logged and the process exits with code 1."""
155
+
156
+ def fake_asyncio_run(coro):
157
+ coro.close()
158
+ raise RuntimeError("boom")
159
+
160
+ exits: list = []
161
+
162
+ def fake_exit(code):
163
+ exits.append(code)
164
+
165
+ monkeypatch.setattr(cli.asyncio, "run", fake_asyncio_run)
166
+ monkeypatch.setattr(cli.sys, "exit", fake_exit)
167
+ cli.run()
168
+ assert exits == [1]
169
+
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # Proxy handlers — verify each MCP method forwards to the upstream session
173
+ # ---------------------------------------------------------------------------
174
+
175
+
176
+ @pytest.mark.asyncio
177
+ async def test_proxy_list_tools_forwards_to_upstream():
178
+ """``list_tools`` returns the upstream's tools verbatim, normalised to list."""
179
+
180
+ upstream = MagicMock()
181
+ upstream.list_tools = AsyncMock(
182
+ return_value=MagicMock(
183
+ tools=(
184
+ types.Tool(name="foo", description="d", inputSchema={"type": "object"}),
185
+ ),
186
+ ),
187
+ )
188
+
189
+ out = await cli._proxy_list_tools(upstream)
190
+ assert [t.name for t in out] == ["foo"]
191
+ upstream.list_tools.assert_awaited_once()
192
+
193
+
194
+ @pytest.mark.asyncio
195
+ async def test_proxy_call_tool_forwards_arguments():
196
+ """``call_tool`` forwards (name, arguments) and returns the content list."""
197
+
198
+ upstream = MagicMock()
199
+ block = types.TextContent(type="text", text="ok")
200
+ upstream.call_tool = AsyncMock(return_value=MagicMock(content=(block,)))
201
+
202
+ out = await cli._proxy_call_tool(upstream, "foo", {"k": "v"})
203
+ assert out == [block]
204
+ upstream.call_tool.assert_awaited_once_with("foo", arguments={"k": "v"})
205
+
206
+
207
+ @pytest.mark.asyncio
208
+ async def test_proxy_call_tool_normalises_none_arguments():
209
+ """If ``arguments`` is None it's coerced to an empty dict before forwarding."""
210
+
211
+ upstream = MagicMock()
212
+ upstream.call_tool = AsyncMock(return_value=MagicMock(content=()))
213
+
214
+ out = await cli._proxy_call_tool(upstream, "foo", None) # type: ignore[arg-type]
215
+ assert out == []
216
+ upstream.call_tool.assert_awaited_once_with("foo", arguments={})
217
+
218
+
219
+ @pytest.mark.asyncio
220
+ async def test_proxy_list_resources_forwards_to_upstream():
221
+ """``list_resources`` returns the upstream's resources verbatim."""
222
+
223
+ upstream = MagicMock()
224
+ res = types.Resource(uri="dj://x", name="x")
225
+ upstream.list_resources = AsyncMock(return_value=MagicMock(resources=(res,)))
226
+
227
+ out = await cli._proxy_list_resources(upstream)
228
+ assert out == [res]
229
+
230
+
231
+ @pytest.mark.asyncio
232
+ async def test_proxy_read_resource_returns_first_text_block():
233
+ """``read_resource`` surfaces the first ``text`` block in the upstream response."""
234
+
235
+ upstream = MagicMock()
236
+ block_no_text = MagicMock(spec=[]) # no .text attribute
237
+ block_with_text = MagicMock()
238
+ block_with_text.text = "hello"
239
+ upstream.read_resource = AsyncMock(
240
+ return_value=MagicMock(contents=[block_no_text, block_with_text]),
241
+ )
242
+
243
+ out = await cli._proxy_read_resource(upstream, "dj://x")
244
+ assert out == "hello"
245
+
246
+
247
+ @pytest.mark.asyncio
248
+ async def test_build_proxy_app_registers_all_four_handlers():
249
+ """``_build_proxy_app`` returns a Server with handlers for every MCP method."""
250
+
251
+ upstream = MagicMock()
252
+ app = cli._build_proxy_app(upstream)
253
+ assert isinstance(app, Server)
254
+ # The MCP SDK stores registered handlers in ``request_handlers`` keyed by
255
+ # the request type. We just need to confirm all four are wired.
256
+ handler_count = len(app.request_handlers)
257
+ assert handler_count >= 4
258
+
259
+
260
+ @pytest.mark.asyncio
261
+ async def test_serve_drives_app_run_with_stdio_streams(monkeypatch):
262
+ """``_serve`` opens stdio_server, builds the proxy app, runs it. Stub
263
+ everything so we just verify the wiring."""
264
+
265
+ sentinel_app = MagicMock()
266
+ sentinel_app.run = AsyncMock()
267
+ sentinel_app.create_initialization_options = MagicMock(return_value="opts")
268
+
269
+ monkeypatch.setattr(cli, "_build_proxy_app", lambda upstream: sentinel_app)
270
+
271
+ class _StdioCtx:
272
+ async def __aenter__(self):
273
+ return ("read-stream", "write-stream")
274
+
275
+ async def __aexit__(self, *exc):
276
+ return False
277
+
278
+ monkeypatch.setattr(cli, "stdio_server", lambda: _StdioCtx())
279
+
280
+ await cli._serve(MagicMock())
281
+ sentinel_app.run.assert_awaited_once_with(
282
+ "read-stream",
283
+ "write-stream",
284
+ "opts",
285
+ )
@@ -1,6 +0,0 @@
1
- """
2
- DataJunction MCP (Model Context Protocol) Server
3
-
4
- This module provides an MCP server for AI agents to interact with the
5
- DataJunction semantic layer.
6
- """
@@ -1,52 +0,0 @@
1
- """
2
- CLI entry point for DataJunction MCP Server
3
- """
4
-
5
- import asyncio
6
- import logging
7
- import sys
8
-
9
- from mcp.server.stdio import stdio_server
10
-
11
- from datajunction.mcp.server import app
12
- from datajunction.mcp.config import get_mcp_settings
13
-
14
- # Configure logging
15
- logging.basicConfig(
16
- level=logging.INFO,
17
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
18
- stream=sys.stderr, # Log to stderr so stdout is clean for MCP protocol
19
- )
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
-
24
- async def main():
25
- """Main entry point for MCP server"""
26
- settings = get_mcp_settings()
27
-
28
- logger.info("Starting DataJunction MCP Server")
29
- logger.info(f"Connecting to DJ API at: {settings.dj_api_url}")
30
-
31
- # Run the MCP server using stdio transport
32
- async with stdio_server() as (read_stream, write_stream):
33
- await app.run(
34
- read_stream,
35
- write_stream,
36
- app.create_initialization_options(),
37
- )
38
-
39
-
40
- def run():
41
- """Synchronous wrapper for async main"""
42
- try:
43
- asyncio.run(main())
44
- except KeyboardInterrupt:
45
- logger.info("MCP Server stopped by user")
46
- except Exception as e:
47
- logger.error(f"MCP Server error: {str(e)}", exc_info=True)
48
- sys.exit(1)
49
-
50
-
51
- if __name__ == "__main__":
52
- run()
@@ -1,27 +0,0 @@
1
- """
2
- Configuration for DataJunction MCP Server
3
- """
4
-
5
- import os
6
- from typing import Optional
7
- from pydantic_settings import BaseSettings
8
-
9
-
10
- class MCPSettings(BaseSettings):
11
- """Settings for MCP server"""
12
-
13
- # DJ API connection
14
- dj_api_url: str = os.getenv("DJ_API_URL", "http://localhost:8000")
15
- dj_api_token: Optional[str] = os.getenv("DJ_API_TOKEN")
16
-
17
- # Optional: Basic auth (if not using token)
18
- dj_username: Optional[str] = os.getenv("DJ_USERNAME")
19
- dj_password: Optional[str] = os.getenv("DJ_PASSWORD")
20
-
21
- # Request timeout
22
- request_timeout: int = 30
23
-
24
-
25
- def get_mcp_settings() -> MCPSettings:
26
- """Get MCP settings singleton"""
27
- return MCPSettings()