woods 1.6.1 → 2.0.0.beta2

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 (274) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2035 -0
  3. data/CONTRIBUTING.md +253 -87
  4. data/README.md +161 -513
  5. data/SECURITY.md +92 -0
  6. data/assets/woods-wordmark-white-with-bg.png +0 -0
  7. data/docs/AGENT_GUIDE.md +204 -0
  8. data/docs/AGENT_SETUP.md +205 -0
  9. data/docs/BACKEND_MATRIX.md +470 -0
  10. data/docs/CONFIGURATION_REFERENCE.md +655 -0
  11. data/docs/CONSOLE_MCP_SETUP.md +829 -0
  12. data/docs/DOCKER_SETUP.md +454 -0
  13. data/docs/EMBEDDING_MODELS.md +136 -0
  14. data/docs/EVALUATION.md +91 -0
  15. data/docs/EXTRACTOR_REFERENCE.md +765 -0
  16. data/docs/FAQ.md +544 -0
  17. data/docs/GETTING_STARTED.md +183 -0
  18. data/docs/INCREMENTAL_EXTRACTION.md +455 -0
  19. data/docs/INTERNALS.md +418 -0
  20. data/docs/MCP_HTTP_TRANSPORT.md +144 -0
  21. data/docs/MCP_SERVERS.md +231 -0
  22. data/docs/MCP_TOOL_COOKBOOK.md +987 -0
  23. data/docs/MCP_WORKTREE_SETUP.md +127 -0
  24. data/docs/NOTION_INTEGRATION.md +283 -0
  25. data/docs/OBSIDIAN_INTEGRATION.md +170 -0
  26. data/docs/PUBLISHED_INDEX.md +213 -0
  27. data/docs/README.md +94 -0
  28. data/docs/RETRIEVAL_GUIDE.md +267 -0
  29. data/docs/TOKEN_BENCHMARK.md +68 -0
  30. data/docs/TROUBLESHOOTING.md +841 -0
  31. data/docs/UNBLOCKED_INTEGRATION.md +279 -0
  32. data/docs/UPGRADING_TO_2.md +321 -0
  33. data/docs/WATCH_DAEMON.md +667 -0
  34. data/docs/WHY_WOODS.md +219 -0
  35. data/exe/woods-console +40 -4
  36. data/exe/woods-console-mcp +21 -35
  37. data/exe/woods-mcp +20 -7
  38. data/exe/woods-mcp-http +80 -11
  39. data/exe/woods-mcp-start +57 -52
  40. data/lib/generators/woods/install_generator.rb +6 -5
  41. data/lib/generators/woods/pgvector_generator.rb +6 -3
  42. data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
  43. data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
  44. data/lib/generators/woods/templates/woods.rb.tt +49 -28
  45. data/lib/tasks/woods.rake +622 -168
  46. data/lib/tasks/woods_checks.rake +107 -0
  47. data/lib/tasks/woods_evaluation.rake +164 -80
  48. data/lib/woods/ast/call_site_extractor.rb +6 -15
  49. data/lib/woods/ast/method_extractor.rb +19 -9
  50. data/lib/woods/ast/parser.rb +54 -8
  51. data/lib/woods/atomic_file.rb +171 -2
  52. data/lib/woods/builder.rb +310 -22
  53. data/lib/woods/cache/cache_middleware.rb +7 -2
  54. data/lib/woods/cache/cache_store.rb +9 -1
  55. data/lib/woods/cache/solid_cache_store.rb +6 -4
  56. data/lib/woods/change_set.rb +88 -0
  57. data/lib/woods/checks/generation_resolution.rb +34 -0
  58. data/lib/woods/checks/moved_messages.rb +186 -0
  59. data/lib/woods/chunking/semantic_chunker.rb +160 -18
  60. data/lib/woods/console/audit_logger.rb +12 -3
  61. data/lib/woods/console/bridge_protocol.rb +3 -16
  62. data/lib/woods/console/connection_manager.rb +51 -136
  63. data/lib/woods/console/dispatch_pipeline.rb +42 -12
  64. data/lib/woods/console/embedded_executor.rb +806 -149
  65. data/lib/woods/console/eval_guard.rb +27 -20
  66. data/lib/woods/console/input_contract.rb +78 -0
  67. data/lib/woods/console/model_validator.rb +29 -1
  68. data/lib/woods/console/rack_middleware.rb +65 -42
  69. data/lib/woods/console/redactor.rb +26 -8
  70. data/lib/woods/console/safe_context.rb +58 -10
  71. data/lib/woods/console/scope_predicate_parser.rb +41 -0
  72. data/lib/woods/console/server.rb +119 -247
  73. data/lib/woods/console/sql_noise_stripper.rb +125 -16
  74. data/lib/woods/console/sql_table_scanner.rb +82 -22
  75. data/lib/woods/console/sql_validator.rb +459 -29
  76. data/lib/woods/console/table_gate.rb +2 -2
  77. data/lib/woods/console/tool_specs.rb +463 -90
  78. data/lib/woods/console/tools/tier1.rb +1 -5
  79. data/lib/woods/console/tools/tier4.rb +18 -9
  80. data/lib/woods/coordination/lock_heartbeat.rb +103 -0
  81. data/lib/woods/coordination/pipeline_lock.rb +263 -53
  82. data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
  83. data/lib/woods/db/migrator.rb +3 -9
  84. data/lib/woods/db/schema_version.rb +47 -2
  85. data/lib/woods/dependency_graph.rb +898 -64
  86. data/lib/woods/embedding/fake.rb +138 -0
  87. data/lib/woods/embedding/indexer.rb +832 -40
  88. data/lib/woods/embedding/openai.rb +77 -19
  89. data/lib/woods/embedding/provider.rb +189 -11
  90. data/lib/woods/embedding/text_preparer.rb +1 -1
  91. data/lib/woods/embedding/token_counter.rb +0 -7
  92. data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
  93. data/lib/woods/evaluation/ablation_executor.rb +67 -0
  94. data/lib/woods/evaluation/ablation_provenance.rb +38 -0
  95. data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
  96. data/lib/woods/evaluation/ablation_runner.rb +173 -0
  97. data/lib/woods/evaluation/ablation_summary.rb +65 -0
  98. data/lib/woods/evaluation/ablation_task.rb +66 -0
  99. data/lib/woods/evaluation/ablation_task_set.rb +77 -0
  100. data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
  101. data/lib/woods/evaluation/ablation_worktree.rb +71 -0
  102. data/lib/woods/evaluation/baseline.rb +60 -0
  103. data/lib/woods/evaluation/baseline_runner.rb +11 -3
  104. data/lib/woods/evaluation/evaluator.rb +41 -8
  105. data/lib/woods/evaluation/query_set.rb +79 -13
  106. data/lib/woods/evaluation/report_generator.rb +20 -1
  107. data/lib/woods/export/unit_facts.rb +0 -11
  108. data/lib/woods/extracted_unit.rb +22 -63
  109. data/lib/woods/extractor.rb +2783 -238
  110. data/lib/woods/extractors/action_cable_extractor.rb +9 -4
  111. data/lib/woods/extractors/ast_source_extraction.rb +20 -2
  112. data/lib/woods/extractors/caching_extractor.rb +46 -12
  113. data/lib/woods/extractors/callback_analyzer.rb +39 -9
  114. data/lib/woods/extractors/component_discovery.rb +123 -0
  115. data/lib/woods/extractors/concern_extractor.rb +17 -3
  116. data/lib/woods/extractors/controller_extractor.rb +389 -29
  117. data/lib/woods/extractors/decorator_extractor.rb +7 -14
  118. data/lib/woods/extractors/engine_extractor.rb +53 -8
  119. data/lib/woods/extractors/event_extractor.rb +55 -4
  120. data/lib/woods/extractors/factory_extractor.rb +49 -11
  121. data/lib/woods/extractors/graphql_extractor.rb +162 -66
  122. data/lib/woods/extractors/i18n_extractor.rb +6 -1
  123. data/lib/woods/extractors/job_extractor.rb +51 -21
  124. data/lib/woods/extractors/lib_extractor.rb +23 -17
  125. data/lib/woods/extractors/line_neutralizer.rb +171 -0
  126. data/lib/woods/extractors/mailer_extractor.rb +9 -1
  127. data/lib/woods/extractors/manager_extractor.rb +19 -2
  128. data/lib/woods/extractors/migration_extractor.rb +22 -11
  129. data/lib/woods/extractors/model_extractor.rb +292 -57
  130. data/lib/woods/extractors/package_extractor.rb +154 -0
  131. data/lib/woods/extractors/phlex_extractor.rb +18 -3
  132. data/lib/woods/extractors/policy_extractor.rb +6 -5
  133. data/lib/woods/extractors/poro_extractor.rb +13 -14
  134. data/lib/woods/extractors/pundit_extractor.rb +3 -3
  135. data/lib/woods/extractors/rails_source_extractor.rb +24 -7
  136. data/lib/woods/extractors/rake_task_extractor.rb +158 -30
  137. data/lib/woods/extractors/reference_patterns.rb +38 -0
  138. data/lib/woods/extractors/route_extractor.rb +58 -2
  139. data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
  140. data/lib/woods/extractors/serializer_extractor.rb +3 -4
  141. data/lib/woods/extractors/service_extractor.rb +11 -1
  142. data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
  143. data/lib/woods/extractors/shared_utility_methods.rb +36 -6
  144. data/lib/woods/extractors/source_nesting.rb +560 -0
  145. data/lib/woods/extractors/state_machine_extractor.rb +30 -18
  146. data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
  147. data/lib/woods/extractors/view_component_extractor.rb +28 -3
  148. data/lib/woods/extractors/view_engines/erb.rb +17 -3
  149. data/lib/woods/feedback/gap_detector.rb +9 -3
  150. data/lib/woods/feedback/store.rb +7 -1
  151. data/lib/woods/filename_utils.rb +29 -1
  152. data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
  153. data/lib/woods/flow_assembler.rb +147 -26
  154. data/lib/woods/flow_document.rb +1 -0
  155. data/lib/woods/flow_precomputer.rb +175 -22
  156. data/lib/woods/gem_mapper.rb +285 -0
  157. data/lib/woods/generation.rb +185 -0
  158. data/lib/woods/git_command.rb +38 -0
  159. data/lib/woods/git_provenance.rb +16 -2
  160. data/lib/woods/graph_analyzer.rb +564 -87
  161. data/lib/woods/index_artifact.rb +93 -23
  162. data/lib/woods/mcp/bearer_auth.rb +102 -13
  163. data/lib/woods/mcp/bootstrap_state.rb +77 -0
  164. data/lib/woods/mcp/bootstrapper.rb +582 -77
  165. data/lib/woods/mcp/config_resolver.rb +66 -6
  166. data/lib/woods/mcp/errors.rb +60 -0
  167. data/lib/woods/mcp/index_reader.rb +836 -117
  168. data/lib/woods/mcp/index_reader_pinning.rb +78 -0
  169. data/lib/woods/mcp/origin_guard.rb +66 -7
  170. data/lib/woods/mcp/protocol_policy.rb +98 -0
  171. data/lib/woods/mcp/provider_probe.rb +45 -6
  172. data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
  173. data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
  174. data/lib/woods/mcp/server.rb +898 -152
  175. data/lib/woods/mcp/tasks/extension.rb +196 -0
  176. data/lib/woods/mcp/tasks/request_capture.rb +45 -0
  177. data/lib/woods/mcp/tasks/store.rb +518 -0
  178. data/lib/woods/mcp/tool_contract.rb +171 -0
  179. data/lib/woods/mcp/tool_response_renderer.rb +7 -0
  180. data/lib/woods/model_name_cache.rb +19 -1
  181. data/lib/woods/notion/client.rb +132 -36
  182. data/lib/woods/notion/exporter.rb +456 -61
  183. data/lib/woods/notion/mappers/column_mapper.rb +34 -5
  184. data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
  185. data/lib/woods/notion/mappers/model_mapper.rb +21 -6
  186. data/lib/woods/notion/mappers/shared.rb +45 -3
  187. data/lib/woods/notion/sync_manifest.rb +258 -0
  188. data/lib/woods/obsidian/errors.rb +6 -0
  189. data/lib/woods/obsidian/name_mapper.rb +40 -24
  190. data/lib/woods/obsidian/vault_exporter.rb +103 -36
  191. data/lib/woods/operator/pipeline_guard.rb +118 -21
  192. data/lib/woods/operator/status_reporter.rb +20 -3
  193. data/lib/woods/path_dispatcher.rb +276 -0
  194. data/lib/woods/payload_store.rb +236 -0
  195. data/lib/woods/published_index/edge_shaper.rb +61 -0
  196. data/lib/woods/published_index/generation_catalog.rb +72 -0
  197. data/lib/woods/published_index/typed_unit_reader.rb +48 -0
  198. data/lib/woods/published_index.rb +287 -0
  199. data/lib/woods/railtie.rb +69 -30
  200. data/lib/woods/railtie_support.rb +167 -0
  201. data/lib/woods/release.rb +12 -0
  202. data/lib/woods/reload_policy.rb +206 -0
  203. data/lib/woods/resilience/circuit_breaker.rb +47 -8
  204. data/lib/woods/resilience/index_validator.rb +296 -10
  205. data/lib/woods/resilience/retryable_provider.rb +71 -6
  206. data/lib/woods/resolved_config.rb +55 -11
  207. data/lib/woods/retrieval/context_assembler.rb +132 -40
  208. data/lib/woods/retrieval/query_classifier.rb +26 -8
  209. data/lib/woods/retrieval/ranker.rb +193 -28
  210. data/lib/woods/retrieval/search_executor.rb +206 -39
  211. data/lib/woods/retriever.rb +317 -71
  212. data/lib/woods/retry_after.rb +22 -2
  213. data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
  214. data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
  215. data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
  216. data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
  217. data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
  218. data/lib/woods/ruby_analyzer.rb +21 -5
  219. data/lib/woods/session_tracer/file_store.rb +138 -19
  220. data/lib/woods/session_tracer/middleware.rb +1 -2
  221. data/lib/woods/session_tracer/redis_store.rb +122 -12
  222. data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
  223. data/lib/woods/session_tracer/session_flow_document.rb +56 -14
  224. data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
  225. data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
  226. data/lib/woods/session_tracer/store.rb +14 -1
  227. data/lib/woods/storage/metadata_store.rb +230 -26
  228. data/lib/woods/storage/pgvector.rb +180 -22
  229. data/lib/woods/storage/qdrant.rb +367 -41
  230. data/lib/woods/storage/snapshotter/metadata.rb +79 -16
  231. data/lib/woods/storage/snapshotter/vector.rb +128 -17
  232. data/lib/woods/storage/snapshotter.rb +23 -5
  233. data/lib/woods/storage/vector_store.rb +49 -8
  234. data/lib/woods/storage_identity.rb +28 -0
  235. data/lib/woods/tasks.rb +53 -2
  236. data/lib/woods/temporal/json_snapshot_store.rb +112 -42
  237. data/lib/woods/temporal/snapshot_store.rb +139 -42
  238. data/lib/woods/unblocked/client.rb +119 -17
  239. data/lib/woods/unblocked/document_builder.rb +34 -2
  240. data/lib/woods/unblocked/exporter.rb +63 -27
  241. data/lib/woods/unblocked/rate_limiter.rb +23 -9
  242. data/lib/woods/unblocked/sync_manifest.rb +16 -8
  243. data/lib/woods/update_check.rb +24 -1
  244. data/lib/woods/util/uuid5.rb +124 -0
  245. data/lib/woods/version.rb +1 -1
  246. data/lib/woods/watch/daemon.rb +1345 -0
  247. data/lib/woods/watch/listen_watcher.rb +81 -0
  248. data/lib/woods/watch/polling_watcher.rb +137 -0
  249. data/lib/woods/watch/status.rb +169 -0
  250. data/lib/woods/watch/tree_scan.rb +163 -0
  251. data/lib/woods/watch/watcher.rb +100 -0
  252. data/lib/woods.rb +138 -9
  253. data/plugin/.claude-plugin/plugin.json +18 -0
  254. data/plugin/hooks/hooks.json +29 -0
  255. data/plugin/hooks/woods-post-edit.sh +226 -0
  256. data/plugin/hooks/woods-session-start.sh +77 -0
  257. data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
  258. data/plugin/skills/woods-diagnose/SKILL.md +75 -0
  259. data/plugin/skills/woods-investigate/SKILL.md +39 -0
  260. data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
  261. data/plugin/skills/woods-setup/SKILL.md +99 -0
  262. metadata +134 -23
  263. data/lib/woods/console/adapters/cache_adapter.rb +0 -58
  264. data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
  265. data/lib/woods/console/adapters/job_adapter.rb +0 -74
  266. data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
  267. data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
  268. data/lib/woods/console/bridge.rb +0 -210
  269. data/lib/woods/formatting/claude_adapter.rb +0 -98
  270. data/lib/woods/formatting/generic_adapter.rb +0 -56
  271. data/lib/woods/formatting/gpt_adapter.rb +0 -64
  272. data/lib/woods/notion/mapper.rb +0 -40
  273. data/lib/woods/observability/health_check.rb +0 -79
  274. data/lib/woods/observability/instrumentation.rb +0 -34
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'store'
4
+
5
+ module Woods
6
+ module MCP
7
+ module Tasks
8
+ # Wiring for the `io.modelcontextprotocol/tasks` extension.
9
+ #
10
+ # The mcp gem does not implement this extension (as of 1.2.0 it still
11
+ # defines neither the `tasks/*` methods nor native task result helpers), so
12
+ # Woods supplies the missing pieces itself:
13
+ #
14
+ # * capability detection, by reading the per-request `_meta` the SDK
15
+ # passes through untouched, and
16
+ # * the `tasks/get` / `tasks/update` RPCs, plus a stable unsupported
17
+ # response for `tasks/cancel`, via `define_custom_method`.
18
+ #
19
+ # Both are deliberately thin. When the SDK grows native support this
20
+ # collapses to deleting {install} and pointing the pipeline tools at the
21
+ # SDK's own task handle; {Store} — the durable part, which is the part
22
+ # that actually buys crash resilience — is unaffected either way.
23
+ module Extension
24
+ # Extension identifier, as advertised in capabilities and as looked for
25
+ # in the client's per-request capabilities.
26
+ EXTENSION_ID = 'io.modelcontextprotocol/tasks'
27
+
28
+ # Reserved `_meta` key carrying the client's per-request capabilities.
29
+ CLIENT_CAPABILITIES_KEY = 'io.modelcontextprotocol/clientCapabilities'
30
+
31
+ # Tasks are an extension to the stateless lifecycle, not a legacy
32
+ # initialize capability. Both the version and explicit extension opt-in
33
+ # must be present on the same request.
34
+ PROTOCOL_VERSION_KEY = 'io.modelcontextprotocol/protocolVersion'
35
+ MODERN_PROTOCOL_VERSION = '2026-07-28'
36
+
37
+ class << self
38
+ # Did this request's client declare support for the Tasks extension?
39
+ #
40
+ # The spec is explicit that a server must never hand a task to a
41
+ # client that did not opt in — such a client would treat the handle as
42
+ # the final result and report a run that had not happened. So this
43
+ # answers **false** for anything it cannot positively confirm:
44
+ # a legacy client with no `_meta`, a modern client that declared other
45
+ # extensions, or a malformed envelope.
46
+ #
47
+ # Params arrive deep-symbolized from the transport but are written
48
+ # with string keys in the spec and in hand-built calls, so both forms
49
+ # are read.
50
+ #
51
+ # @param params [Hash, nil] the `tools/call` params
52
+ # @return [Boolean]
53
+ def client_opted_in?(params)
54
+ version = dig_indifferent(params, :_meta, PROTOCOL_VERSION_KEY)
55
+ return false unless version == MODERN_PROTOCOL_VERSION
56
+
57
+ extensions = dig_indifferent(params, :_meta, CLIENT_CAPABILITIES_KEY, :extensions)
58
+ return false unless extensions.is_a?(Hash)
59
+
60
+ extensions.key?(EXTENSION_ID) || extensions.key?(EXTENSION_ID.to_sym)
61
+ end
62
+
63
+ # The `CreateTaskResult` returned in place of a synchronous result.
64
+ #
65
+ # @param task [Store::Task]
66
+ # @return [Hash]
67
+ def create_task_result(task)
68
+ task.to_h.merge(resultType: 'task')
69
+ end
70
+
71
+ # Register the `tasks/*` RPCs and advertise the extension.
72
+ #
73
+ # @param server [MCP::Server]
74
+ # @param store [Store]
75
+ # @return [MCP::Server]
76
+ def install(server, store:)
77
+ advertise(server)
78
+ define_get(server, store)
79
+ define_unsupported_cancel(server)
80
+ define_update(server, store)
81
+ server
82
+ end
83
+
84
+ private
85
+
86
+ def advertise(server)
87
+ capabilities = server.capabilities || {}
88
+ extensions = capabilities[:extensions] || {}
89
+ server.capabilities = capabilities.merge(
90
+ extensions: extensions.merge(EXTENSION_ID => {})
91
+ )
92
+ end
93
+
94
+ def define_get(server, store)
95
+ server.define_custom_method(method_name: 'tasks/get') do |params, server_context:|
96
+ require_tasks_capability!(params, server_context)
97
+ task = Extension.send(:load_task, store, params)
98
+ unless task
99
+ raise ::MCP::Server::RequestHandlerError.new(
100
+ 'Unknown or expired taskId.', params, error_type: :invalid_params
101
+ )
102
+ end
103
+
104
+ task.to_h.merge(resultType: 'complete')
105
+ end
106
+ end
107
+
108
+ def define_unsupported_cancel(server)
109
+ server.define_custom_method(method_name: 'tasks/cancel') do |params, server_context:|
110
+ require_tasks_capability!(params, server_context)
111
+ raise ::MCP::Server::RequestHandlerError.new(
112
+ 'Method not found', params,
113
+ error_type: :method_not_found,
114
+ error_code: ::JsonRpcHandler::ErrorCode::METHOD_NOT_FOUND,
115
+ error_data: 'Task cancellation is not supported by Woods.'
116
+ )
117
+ end
118
+ end
119
+
120
+ def define_update(server, store)
121
+ server.define_custom_method(method_name: 'tasks/update') do |params, server_context:|
122
+ require_tasks_capability!(params, server_context)
123
+ # No Woods task ever enters `input_required` — nothing in the
124
+ # index pipeline pauses for user input — so there are never
125
+ # outstanding `inputRequests` to satisfy and the spec's guidance
126
+ # ("ignore responses for unknown or already-satisfied keys")
127
+ # makes this an acknowledgement. The method exists because the
128
+ # extension advertises it, and a client that probes it should get
129
+ # an answer rather than "method not found".
130
+ task = Extension.send(:load_task, store, params)
131
+ unless task
132
+ raise ::MCP::Server::RequestHandlerError.new(
133
+ 'Unknown or expired taskId.', params, error_type: :invalid_params
134
+ )
135
+ end
136
+
137
+ { resultType: 'complete' }
138
+ end
139
+ end
140
+
141
+ def require_tasks_capability!(params, server_context)
142
+ return if server_context&.protocol_version == MODERN_PROTOCOL_VERSION &&
143
+ client_capabilities_include_tasks?(server_context.client_capabilities)
144
+ return if client_opted_in?(params)
145
+
146
+ required = { extensions: { EXTENSION_ID => {} } }
147
+ raise ::MCP::Server::MissingRequiredClientCapabilityError.new(required, params)
148
+ end
149
+
150
+ def client_capabilities_include_tasks?(capabilities)
151
+ return false unless capabilities.is_a?(Hash)
152
+
153
+ extensions = capabilities[:extensions] || capabilities['extensions']
154
+ return false unless extensions.is_a?(Hash)
155
+
156
+ extensions.key?(EXTENSION_ID) || extensions.key?(EXTENSION_ID.to_sym)
157
+ end
158
+
159
+ # Custom-method blocks are instance-exec'd without access to module
160
+ # privates, so this is defined on the module itself and reached
161
+ # through the closure over `Extension`.
162
+ def task_id_from(params)
163
+ return nil unless params.is_a?(Hash)
164
+
165
+ params[:taskId] || params['taskId']
166
+ end
167
+
168
+ def load_task(store, params)
169
+ store.get(task_id_from(params))
170
+ rescue Store::CorruptRecordError => e
171
+ task_id = task_id_from(params)
172
+ raise ::MCP::Server::RequestHandlerError.new(
173
+ 'Task record is unavailable or malformed.', params,
174
+ error_type: :internal_error,
175
+ error_code: ::JsonRpcHandler::ErrorCode::INTERNAL_ERROR,
176
+ error_data: { error_code: 'corrupt_task_record', taskId: task_id },
177
+ original_error: e
178
+ )
179
+ end
180
+
181
+ # Reads a nested value tolerating symbol/string keys at every level.
182
+ def dig_indifferent(hash, *keys)
183
+ keys.reduce(hash) do |node, key|
184
+ return nil unless node.is_a?(Hash)
185
+
186
+ value = node[key]
187
+ value = node[key.to_s] if value.nil?
188
+ value = node[key.to_sym] if value.nil? && key.respond_to?(:to_sym)
189
+ value
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'extension'
4
+
5
+ module Woods
6
+ module MCP
7
+ module Tasks
8
+ # Makes the current request's Tasks opt-in visible to a tool handler.
9
+ #
10
+ # A tool block is called with the request's `arguments` and nothing else —
11
+ # the surrounding params, where `_meta` lives, never reach it. But whether
12
+ # to answer with a task handle instead of a synchronous result is a
13
+ # property of the *client*, not of the arguments, so the handler has to be
14
+ # able to ask.
15
+ #
16
+ # Prepended onto a built server instance so it wraps `call_tool`, the same
17
+ # seam {VersionAwareToolDispatch} uses (and which the HTTP and stdio
18
+ # transports both dispatch through).
19
+ #
20
+ # State is thread-local because `woods-mcp-http` runs tool handlers on its
21
+ # Rack server's request threads, and it is cleared in an `ensure` because
22
+ # those threads are pooled and reused — a leaked flag would make the next
23
+ # call on that thread hand a task to a client that never opted in and
24
+ # cannot poll for it.
25
+ module RequestCapture
26
+ THREAD_KEY = :woods_mcp_tasks_requested
27
+
28
+ # @return [Boolean] whether the in-flight request's client declared the
29
+ # Tasks extension. False outside any request.
30
+ def self.tasks_requested?
31
+ Thread.current[THREAD_KEY] == true
32
+ end
33
+
34
+ # @param request [Hash] the `tools/call` params, carrying `_meta`
35
+ def call_tool(request, **)
36
+ previous = Thread.current[THREAD_KEY]
37
+ Thread.current[THREAD_KEY] = Extension.client_opted_in?(request)
38
+ super
39
+ ensure
40
+ Thread.current[THREAD_KEY] = previous
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end