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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2035 -0
- data/CONTRIBUTING.md +253 -87
- data/README.md +161 -513
- data/SECURITY.md +92 -0
- data/assets/woods-wordmark-white-with-bg.png +0 -0
- data/docs/AGENT_GUIDE.md +204 -0
- data/docs/AGENT_SETUP.md +205 -0
- data/docs/BACKEND_MATRIX.md +470 -0
- data/docs/CONFIGURATION_REFERENCE.md +655 -0
- data/docs/CONSOLE_MCP_SETUP.md +829 -0
- data/docs/DOCKER_SETUP.md +454 -0
- data/docs/EMBEDDING_MODELS.md +136 -0
- data/docs/EVALUATION.md +91 -0
- data/docs/EXTRACTOR_REFERENCE.md +765 -0
- data/docs/FAQ.md +544 -0
- data/docs/GETTING_STARTED.md +183 -0
- data/docs/INCREMENTAL_EXTRACTION.md +455 -0
- data/docs/INTERNALS.md +418 -0
- data/docs/MCP_HTTP_TRANSPORT.md +144 -0
- data/docs/MCP_SERVERS.md +231 -0
- data/docs/MCP_TOOL_COOKBOOK.md +987 -0
- data/docs/MCP_WORKTREE_SETUP.md +127 -0
- data/docs/NOTION_INTEGRATION.md +283 -0
- data/docs/OBSIDIAN_INTEGRATION.md +170 -0
- data/docs/PUBLISHED_INDEX.md +213 -0
- data/docs/README.md +94 -0
- data/docs/RETRIEVAL_GUIDE.md +267 -0
- data/docs/TOKEN_BENCHMARK.md +68 -0
- data/docs/TROUBLESHOOTING.md +841 -0
- data/docs/UNBLOCKED_INTEGRATION.md +279 -0
- data/docs/UPGRADING_TO_2.md +321 -0
- data/docs/WATCH_DAEMON.md +667 -0
- data/docs/WHY_WOODS.md +219 -0
- data/exe/woods-console +40 -4
- data/exe/woods-console-mcp +21 -35
- data/exe/woods-mcp +20 -7
- data/exe/woods-mcp-http +80 -11
- data/exe/woods-mcp-start +57 -52
- data/lib/generators/woods/install_generator.rb +6 -5
- data/lib/generators/woods/pgvector_generator.rb +6 -3
- data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
- data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
- data/lib/generators/woods/templates/woods.rb.tt +49 -28
- data/lib/tasks/woods.rake +622 -168
- data/lib/tasks/woods_checks.rake +107 -0
- data/lib/tasks/woods_evaluation.rake +164 -80
- data/lib/woods/ast/call_site_extractor.rb +6 -15
- data/lib/woods/ast/method_extractor.rb +19 -9
- data/lib/woods/ast/parser.rb +54 -8
- data/lib/woods/atomic_file.rb +171 -2
- data/lib/woods/builder.rb +310 -22
- data/lib/woods/cache/cache_middleware.rb +7 -2
- data/lib/woods/cache/cache_store.rb +9 -1
- data/lib/woods/cache/solid_cache_store.rb +6 -4
- data/lib/woods/change_set.rb +88 -0
- data/lib/woods/checks/generation_resolution.rb +34 -0
- data/lib/woods/checks/moved_messages.rb +186 -0
- data/lib/woods/chunking/semantic_chunker.rb +160 -18
- data/lib/woods/console/audit_logger.rb +12 -3
- data/lib/woods/console/bridge_protocol.rb +3 -16
- data/lib/woods/console/connection_manager.rb +51 -136
- data/lib/woods/console/dispatch_pipeline.rb +42 -12
- data/lib/woods/console/embedded_executor.rb +806 -149
- data/lib/woods/console/eval_guard.rb +27 -20
- data/lib/woods/console/input_contract.rb +78 -0
- data/lib/woods/console/model_validator.rb +29 -1
- data/lib/woods/console/rack_middleware.rb +65 -42
- data/lib/woods/console/redactor.rb +26 -8
- data/lib/woods/console/safe_context.rb +58 -10
- data/lib/woods/console/scope_predicate_parser.rb +41 -0
- data/lib/woods/console/server.rb +119 -247
- data/lib/woods/console/sql_noise_stripper.rb +125 -16
- data/lib/woods/console/sql_table_scanner.rb +82 -22
- data/lib/woods/console/sql_validator.rb +459 -29
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/console/tool_specs.rb +463 -90
- data/lib/woods/console/tools/tier1.rb +1 -5
- data/lib/woods/console/tools/tier4.rb +18 -9
- data/lib/woods/coordination/lock_heartbeat.rb +103 -0
- data/lib/woods/coordination/pipeline_lock.rb +263 -53
- data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
- data/lib/woods/db/migrator.rb +3 -9
- data/lib/woods/db/schema_version.rb +47 -2
- data/lib/woods/dependency_graph.rb +898 -64
- data/lib/woods/embedding/fake.rb +138 -0
- data/lib/woods/embedding/indexer.rb +832 -40
- data/lib/woods/embedding/openai.rb +77 -19
- data/lib/woods/embedding/provider.rb +189 -11
- data/lib/woods/embedding/text_preparer.rb +1 -1
- data/lib/woods/embedding/token_counter.rb +0 -7
- data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
- data/lib/woods/evaluation/ablation_executor.rb +67 -0
- data/lib/woods/evaluation/ablation_provenance.rb +38 -0
- data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
- data/lib/woods/evaluation/ablation_runner.rb +173 -0
- data/lib/woods/evaluation/ablation_summary.rb +65 -0
- data/lib/woods/evaluation/ablation_task.rb +66 -0
- data/lib/woods/evaluation/ablation_task_set.rb +77 -0
- data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
- data/lib/woods/evaluation/ablation_worktree.rb +71 -0
- data/lib/woods/evaluation/baseline.rb +60 -0
- data/lib/woods/evaluation/baseline_runner.rb +11 -3
- data/lib/woods/evaluation/evaluator.rb +41 -8
- data/lib/woods/evaluation/query_set.rb +79 -13
- data/lib/woods/evaluation/report_generator.rb +20 -1
- data/lib/woods/export/unit_facts.rb +0 -11
- data/lib/woods/extracted_unit.rb +22 -63
- data/lib/woods/extractor.rb +2783 -238
- data/lib/woods/extractors/action_cable_extractor.rb +9 -4
- data/lib/woods/extractors/ast_source_extraction.rb +20 -2
- data/lib/woods/extractors/caching_extractor.rb +46 -12
- data/lib/woods/extractors/callback_analyzer.rb +39 -9
- data/lib/woods/extractors/component_discovery.rb +123 -0
- data/lib/woods/extractors/concern_extractor.rb +17 -3
- data/lib/woods/extractors/controller_extractor.rb +389 -29
- data/lib/woods/extractors/decorator_extractor.rb +7 -14
- data/lib/woods/extractors/engine_extractor.rb +53 -8
- data/lib/woods/extractors/event_extractor.rb +55 -4
- data/lib/woods/extractors/factory_extractor.rb +49 -11
- data/lib/woods/extractors/graphql_extractor.rb +162 -66
- data/lib/woods/extractors/i18n_extractor.rb +6 -1
- data/lib/woods/extractors/job_extractor.rb +51 -21
- data/lib/woods/extractors/lib_extractor.rb +23 -17
- data/lib/woods/extractors/line_neutralizer.rb +171 -0
- data/lib/woods/extractors/mailer_extractor.rb +9 -1
- data/lib/woods/extractors/manager_extractor.rb +19 -2
- data/lib/woods/extractors/migration_extractor.rb +22 -11
- data/lib/woods/extractors/model_extractor.rb +292 -57
- data/lib/woods/extractors/package_extractor.rb +154 -0
- data/lib/woods/extractors/phlex_extractor.rb +18 -3
- data/lib/woods/extractors/policy_extractor.rb +6 -5
- data/lib/woods/extractors/poro_extractor.rb +13 -14
- data/lib/woods/extractors/pundit_extractor.rb +3 -3
- data/lib/woods/extractors/rails_source_extractor.rb +24 -7
- data/lib/woods/extractors/rake_task_extractor.rb +158 -30
- data/lib/woods/extractors/reference_patterns.rb +38 -0
- data/lib/woods/extractors/route_extractor.rb +58 -2
- data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
- data/lib/woods/extractors/serializer_extractor.rb +3 -4
- data/lib/woods/extractors/service_extractor.rb +11 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
- data/lib/woods/extractors/shared_utility_methods.rb +36 -6
- data/lib/woods/extractors/source_nesting.rb +560 -0
- data/lib/woods/extractors/state_machine_extractor.rb +30 -18
- data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
- data/lib/woods/extractors/view_component_extractor.rb +28 -3
- data/lib/woods/extractors/view_engines/erb.rb +17 -3
- data/lib/woods/feedback/gap_detector.rb +9 -3
- data/lib/woods/feedback/store.rb +7 -1
- data/lib/woods/filename_utils.rb +29 -1
- data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
- data/lib/woods/flow_assembler.rb +147 -26
- data/lib/woods/flow_document.rb +1 -0
- data/lib/woods/flow_precomputer.rb +175 -22
- data/lib/woods/gem_mapper.rb +285 -0
- data/lib/woods/generation.rb +185 -0
- data/lib/woods/git_command.rb +38 -0
- data/lib/woods/git_provenance.rb +16 -2
- data/lib/woods/graph_analyzer.rb +564 -87
- data/lib/woods/index_artifact.rb +93 -23
- data/lib/woods/mcp/bearer_auth.rb +102 -13
- data/lib/woods/mcp/bootstrap_state.rb +77 -0
- data/lib/woods/mcp/bootstrapper.rb +582 -77
- data/lib/woods/mcp/config_resolver.rb +66 -6
- data/lib/woods/mcp/errors.rb +60 -0
- data/lib/woods/mcp/index_reader.rb +836 -117
- data/lib/woods/mcp/index_reader_pinning.rb +78 -0
- data/lib/woods/mcp/origin_guard.rb +66 -7
- data/lib/woods/mcp/protocol_policy.rb +98 -0
- data/lib/woods/mcp/provider_probe.rb +45 -6
- data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
- data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
- data/lib/woods/mcp/server.rb +898 -152
- data/lib/woods/mcp/tasks/extension.rb +196 -0
- data/lib/woods/mcp/tasks/request_capture.rb +45 -0
- data/lib/woods/mcp/tasks/store.rb +518 -0
- data/lib/woods/mcp/tool_contract.rb +171 -0
- data/lib/woods/mcp/tool_response_renderer.rb +7 -0
- data/lib/woods/model_name_cache.rb +19 -1
- data/lib/woods/notion/client.rb +132 -36
- data/lib/woods/notion/exporter.rb +456 -61
- data/lib/woods/notion/mappers/column_mapper.rb +34 -5
- data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
- data/lib/woods/notion/mappers/model_mapper.rb +21 -6
- data/lib/woods/notion/mappers/shared.rb +45 -3
- data/lib/woods/notion/sync_manifest.rb +258 -0
- data/lib/woods/obsidian/errors.rb +6 -0
- data/lib/woods/obsidian/name_mapper.rb +40 -24
- data/lib/woods/obsidian/vault_exporter.rb +103 -36
- data/lib/woods/operator/pipeline_guard.rb +118 -21
- data/lib/woods/operator/status_reporter.rb +20 -3
- data/lib/woods/path_dispatcher.rb +276 -0
- data/lib/woods/payload_store.rb +236 -0
- data/lib/woods/published_index/edge_shaper.rb +61 -0
- data/lib/woods/published_index/generation_catalog.rb +72 -0
- data/lib/woods/published_index/typed_unit_reader.rb +48 -0
- data/lib/woods/published_index.rb +287 -0
- data/lib/woods/railtie.rb +69 -30
- data/lib/woods/railtie_support.rb +167 -0
- data/lib/woods/release.rb +12 -0
- data/lib/woods/reload_policy.rb +206 -0
- data/lib/woods/resilience/circuit_breaker.rb +47 -8
- data/lib/woods/resilience/index_validator.rb +296 -10
- data/lib/woods/resilience/retryable_provider.rb +71 -6
- data/lib/woods/resolved_config.rb +55 -11
- data/lib/woods/retrieval/context_assembler.rb +132 -40
- data/lib/woods/retrieval/query_classifier.rb +26 -8
- data/lib/woods/retrieval/ranker.rb +193 -28
- data/lib/woods/retrieval/search_executor.rb +206 -39
- data/lib/woods/retriever.rb +317 -71
- data/lib/woods/retry_after.rb +22 -2
- data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
- data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
- data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
- data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
- data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
- data/lib/woods/ruby_analyzer.rb +21 -5
- data/lib/woods/session_tracer/file_store.rb +138 -19
- data/lib/woods/session_tracer/middleware.rb +1 -2
- data/lib/woods/session_tracer/redis_store.rb +122 -12
- data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
- data/lib/woods/session_tracer/session_flow_document.rb +56 -14
- data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
- data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
- data/lib/woods/session_tracer/store.rb +14 -1
- data/lib/woods/storage/metadata_store.rb +230 -26
- data/lib/woods/storage/pgvector.rb +180 -22
- data/lib/woods/storage/qdrant.rb +367 -41
- data/lib/woods/storage/snapshotter/metadata.rb +79 -16
- data/lib/woods/storage/snapshotter/vector.rb +128 -17
- data/lib/woods/storage/snapshotter.rb +23 -5
- data/lib/woods/storage/vector_store.rb +49 -8
- data/lib/woods/storage_identity.rb +28 -0
- data/lib/woods/tasks.rb +53 -2
- data/lib/woods/temporal/json_snapshot_store.rb +112 -42
- data/lib/woods/temporal/snapshot_store.rb +139 -42
- data/lib/woods/unblocked/client.rb +119 -17
- data/lib/woods/unblocked/document_builder.rb +34 -2
- data/lib/woods/unblocked/exporter.rb +63 -27
- data/lib/woods/unblocked/rate_limiter.rb +23 -9
- data/lib/woods/unblocked/sync_manifest.rb +16 -8
- data/lib/woods/update_check.rb +24 -1
- data/lib/woods/util/uuid5.rb +124 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/daemon.rb +1345 -0
- data/lib/woods/watch/listen_watcher.rb +81 -0
- data/lib/woods/watch/polling_watcher.rb +137 -0
- data/lib/woods/watch/status.rb +169 -0
- data/lib/woods/watch/tree_scan.rb +163 -0
- data/lib/woods/watch/watcher.rb +100 -0
- data/lib/woods.rb +138 -9
- data/plugin/.claude-plugin/plugin.json +18 -0
- data/plugin/hooks/hooks.json +29 -0
- data/plugin/hooks/woods-post-edit.sh +226 -0
- data/plugin/hooks/woods-session-start.sh +77 -0
- data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
- data/plugin/skills/woods-diagnose/SKILL.md +75 -0
- data/plugin/skills/woods-investigate/SKILL.md +39 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
- data/plugin/skills/woods-setup/SKILL.md +99 -0
- metadata +134 -23
- data/lib/woods/console/adapters/cache_adapter.rb +0 -58
- data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
- data/lib/woods/console/adapters/job_adapter.rb +0 -74
- data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
- data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
- data/lib/woods/console/bridge.rb +0 -210
- data/lib/woods/formatting/claude_adapter.rb +0 -98
- data/lib/woods/formatting/generic_adapter.rb +0 -56
- data/lib/woods/formatting/gpt_adapter.rb +0 -64
- data/lib/woods/notion/mapper.rb +0 -40
- data/lib/woods/observability/health_check.rb +0 -79
- 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
|