24#include <nlohmann/json.hpp>
83 const std::string& args_json)
const override;
96 void apply_todo_action(
const std::string& action,
97 const nlohmann::json& args);
99 std::vector<TodoItem> items_;
107 std::string format_list()
const;
118 const std::string& )
const {
128std::string TodoTool::format_list()
const {
129 if (items_.empty()) {
133 for (
size_t i = 0; i < items_.size(); ++i) {
134 out += std::to_string(i) +
". [" +
135 items_[i].status +
"] " +
136 items_[i].content +
"\n";
155void TodoTool::apply_todo_action(
const std::string& action,
156 const nlohmann::json& args) {
157 if (action ==
"add") {
158 std::string content = args.at(
"content").get<std::string>();
159 items_.push_back({content,
"pending"});
160 logger->info(
"[todo] add: {}", content);
161 }
else if (action ==
"update") {
162 auto idx = args.at(
"index").get<
size_t>();
163 if (idx < items_.size()) {
164 items_[idx].status = args.value(
"status", items_[idx].status);
165 items_[idx].content = args.value(
"content", items_[idx].content);
166 logger->info(
"[todo] update #{}: {}", idx, items_[idx].status);
168 }
else if (action ==
"remove") {
169 auto idx = args.at(
"index").get<
size_t>();
170 if (idx < items_.size()) {
171 logger->info(
"[todo] remove #{}", idx);
172 items_.erase(items_.begin() +
static_cast<ptrdiff_t
>(idx));
186 auto args = nlohmann::json::parse(args_json);
187 std::string action = args.at(
"action").get<std::string>();
189 apply_todo_action(action, args);
191 nlohmann::json result;
192 result[
"todo_state"] = format_list();
193 result[
"action"] = action;
199 return {result.dump(), {anchor_d, notify_d}};
219 const std::vector<std::string>& tier_names);
246 const std::vector<std::string>& tier_names)
250 schema[
"properties"][
"target"][
"enum"] = tier_names;
253 logger->info(
"[delegate] patched enum with {} tiers",
268 auto args = nlohmann::json::parse(args_json);
269 std::string target = args.at(
"target").get<std::string>();
270 std::string task = args.at(
"task").get<std::string>();
271 int max_turns = args.value(
"max_turns", -1);
273 logger->info(
"[delegate] target='{}' task='{}' max_turns={}",
274 target, task, max_turns);
276 nlohmann::json result;
277 result[
"action"] =
"delegate";
278 result[
"target"] = target;
279 result[
"task"] = task;
280 result[
"max_turns"] = max_turns;
288 return {result.dump(), {delegate_d, stop_d}};
308 const std::vector<std::string>& tier_names);
320 std::vector<std::string> tier_names_;
338 const std::vector<std::string>& tier_names)
339 :
ToolBase(std::move(def)), tier_names_(tier_names) {
342 schema[
"properties"][
"stages"][
"items"][
"enum"] = tier_names;
345 logger->info(
"[pipeline] patched enum with {} tiers",
366 auto args = nlohmann::json::parse(args_json);
367 auto stages = args.at(
"stages").get<std::vector<std::string>>();
368 std::string task = args.at(
"task").get<std::string>();
370 if (stages.size() < 2) {
371 logger->warn(
"[pipeline] rejected: fewer than 2 stages");
372 return {
"Error: pipeline requires at least 2 stages", {}};
375 for (
const auto& s : stages) {
376 auto it = std::find(tier_names_.begin(), tier_names_.end(), s);
377 if (it == tier_names_.end()) {
378 logger->warn(
"[pipeline] invalid stage: '{}'", s);
379 auto valid = nlohmann::json(tier_names_).dump();
380 return {
"Error: unknown stage \"" + s
381 +
"\". Valid stages: " + valid, {}};
385 logger->info(
"[pipeline] stages={} task='{}'",
386 stages.size(), task);
388 nlohmann::json result;
389 result[
"action"] =
"pipeline";
390 result[
"stages"] = stages;
391 result[
"task"] = task;
399 return {result.dump(), {pipeline_d, stop_d}};
451 auto args = nlohmann::json::parse(args_json);
452 std::string summary = args.at(
"summary").get<std::string>();
453 bool coverage_gap = args.value(
"coverage_gap",
false);
454 std::string gap_description =
455 args.value(
"gap_description", std::string{});
456 std::vector<std::string> suggested;
457 if (args.contains(
"suggested_files")
458 && args[
"suggested_files"].is_array()) {
459 suggested = args[
"suggested_files"]
460 .get<std::vector<std::string>>();
468 if (coverage_gap && gap_description.empty()) {
470 err[
"error"] =
"missing_gap_description";
472 "coverage_gap=true requires a non-empty gap_description "
473 "(what's missing from this answer and why).";
474 return {err.dump(), {}};
477 logger->info(
"[complete] summary='{}' coverage_gap={} "
478 "gap_description_len={} suggested_files={}",
479 summary, coverage_gap,
480 gap_description.size(), suggested.size());
482 nlohmann::json result;
483 result[
"action"] =
"complete";
484 result[
"summary"] = entropic::mcp::sanitize_utf8(summary);
485 result[
"coverage_gap"] = coverage_gap;
486 result[
"gap_description"] = entropic::mcp::sanitize_utf8(gap_description);
487 result[
"suggested_files"] = suggested;
495 return {result.dump(), {complete_d, stop_d}};
531 "Switch inference phase",
532 R
"({"type":"object","properties":{"phase":{"type":"string"}},"required":["phase"]})"}) {}
543 const std::string& args_json) {
544 auto args = nlohmann::json::parse(args_json);
545 std::string phase = args.at(
"phase").get<std::string>();
547 logger->info(
"[phase_change] phase='{}'", phase);
549 nlohmann::json result;
550 result[
"action"] =
"phase_change";
551 result[
"phase"] = phase;
556 return {result.dump(), {phase_d}};
595 const std::string& args_json) {
596 auto args = nlohmann::json::parse(args_json);
598 static constexpr int default_keep = 2;
599 int keep_recent = args.value(
"keep_recent", default_keep);
601 logger->info(
"[prune_context] keep_recent={}", keep_recent);
603 nlohmann::json result;
604 result[
"action"] =
"prune_context";
605 result[
"keep_recent"] = keep_recent;
610 return {result.dump(), {prune_d}};
679 char* (*fn)(
void*),
void* ud) {
684 if (raw ==
nullptr) {
687 std::string result(raw);
702 char* (*fn)(
int,
void*),
int max_entries,
void* ud) {
706 char* raw = fn(max_entries, ud);
707 if (raw ==
nullptr) {
710 std::string result(raw);
725 char* (*fn)(
const char*,
void*),
726 const char* section,
void* ud) {
730 char* raw = fn(section, ud);
731 if (raw ==
nullptr) {
734 std::string result(raw);
750 bool include_docs,
int history_limit) {
754 auto now = std::chrono::system_clock::now();
755 auto ms = std::chrono::duration_cast<
756 std::chrono::milliseconds>(
757 now.time_since_epoch()).
count();
758 snap[
"snapshot_timestamp_ms"] = ms;
760 snap[
"engine"] = nlohmann::json::parse(
762 snap[
"config"] = nlohmann::json::parse(
764 snap[
"identities"] = nlohmann::json::parse(
766 snap[
"tools"] = nlohmann::json::parse(
768 snap[
"history"] = nlohmann::json::parse(
771 snap[
"metrics"] = nlohmann::json::parse(
778 snap[
"docs"] =
nullptr;
799 if (provider_ ==
nullptr) {
800 logger->error(
"[diagnose] no state provider set");
801 return {
"Error: engine state provider not configured", {}};
804 auto args = nlohmann::json::parse(args_json);
805 bool include_docs = args.value(
"include_docs",
false);
806 int history_limit = args.value(
"history_limit", 20);
808 logger->info(
"[diagnose] include_docs={} history_limit={}",
809 include_docs, history_limit);
812 *provider_, include_docs, history_limit);
813 return {snap.dump(), {}};
930 const nlohmann::json& j) {
931 std::vector<std::string> keys;
932 for (
auto it = j.begin(); it != j.end(); ++it) {
933 keys.push_back(it.key());
946 const nlohmann::json& j) {
947 std::vector<std::string> names;
948 for (
const auto& item : j) {
949 if (item.is_object() && item.contains(
"name")) {
950 names.push_back(item[
"name"].get<std::string>());
967 for (
const auto& k : keys) {
968 if (!result.empty()) { result +=
", "; }
984 const std::string& json_str,
985 const std::string& key,
986 const std::string& label) {
987 auto j = nlohmann::json::parse(json_str);
989 if (j.is_object() && j.contains(key)) {
990 return j[key].dump();
993 for (
const auto& item : j) {
994 if (item.value(
"name",
"") == key) {
999 return "Error: " + label +
" '" + key
1000 +
"' not found. Available: "
1015 char* (*fn)(
void*),
void* ud,
1016 const std::string& key,
const std::string& label) {
1036 const std::string& target,
1037 const std::string& key,
1038 std::string& result) {
1040 if (target ==
"state") {
1042 }
else if (target ==
"metrics") {
1044 }
else if (target ==
"history") {
1045 int limit = key.empty() ? 10 : std::atoi(key.c_str());
1048 }
else if (target ==
"docs") {
1049 const char* sec = key.empty() ? nullptr : key.c_str();
1069 const std::string& target,
1070 const std::string& key,
1071 std::string& result) {
1072 if (target ==
"config") {
1075 }
else if (target ==
"identity") {
1078 }
else if (target ==
"tool") {
1098 const std::string& target,
1099 const std::string& key) {
1108 return "Error: unknown target '" + target
1109 +
"'. Supported: config, identity, tool, state, "
1110 "metrics, history, docs";
1142 if (provider_ ==
nullptr) {
1143 logger->error(
"[inspect] no state provider set");
1144 return {
"Error: engine state provider not configured", {}};
1147 auto args = nlohmann::json::parse(args_json,
nullptr,
false);
1153 if (args.is_discarded() || !args.is_object()) {
1154 args = nlohmann::json::object();
1156 std::string target = args.value(
"target",
"");
1157 std::string key = args.value(
"key",
"");
1159 if (target.empty()) {
1160 logger->info(
"[inspect] full state dump (no target)");
1166 logger->info(
"[inspect] target='{}' key='{}'", target, key);
1168 return {result, {}};
1186 const std::string& args_json) {
1187 if (provider_ ==
nullptr) {
1188 logger->error(
"[context_inspect] no state provider set");
1189 return {
"Error: engine state provider not configured", {}};
1192 auto args = nlohmann::json::parse(args_json,
nullptr,
false);
1193 int max_messages = args.value(
"max_messages", 0);
1195 logger->info(
"[context_inspect] max_messages={}", max_messages);
1198 return {result, {}};
1270 auto args = nlohmann::json::parse(args_json,
nullptr,
false);
1272 if (args.is_discarded() || !args.is_object()) {
1273 body = R
"({"error":"invalid args: object with 'query' required"})";
1275 std::string query = args.value(
"query",
"");
1276 int max_results = args.value(
"max_results", 3);
1277 if (query.empty()) {
1278 body = R
"({"error":"'query' is required and must be non-empty"})";
1279 } else if (provider_ ==
nullptr
1281 logger->warn(
"[followup] no search_delegations provider "
1283 body = R
"({"error":"delegation storage not available"})";
1285 logger->info(
"[followup] query='{}' max_results={}",
1286 query, max_results);
1288 query.c_str(), max_results, provider_->
user_data);
1289 if (raw ==
nullptr) {
1290 body = R
"({"results":[]})";
1346 auto args = nlohmann::json::parse(args_json,
nullptr,
false);
1347 if (args.is_discarded() || !args.is_object()) {
1348 return {R
"({"error":"invalid args: object required"})", {}};
1350 std::string delegation_id = args.value("delegation_id",
"");
1351 std::string task = args.value(
"task",
"");
1352 int max_turns = args.value(
"max_turns", -1);
1353 if (delegation_id.empty() || task.empty()) {
1354 return {R
"({"error":"'delegation_id' and 'task' are required"})",
1357 logger->info("[resume_delegation] id='{}' task='{}' max_turns={}",
1358 delegation_id, task, max_turns);
1360 nlohmann::json result;
1361 result[
"action"] =
"resume_delegation";
1362 result[
"delegation_id"] = delegation_id;
1363 result[
"task"] = task;
1364 result[
"max_turns"] = max_turns;
1370 return {result.dump(), {delegate_d, stop_d}};
1388int EntropicServer::register_core_tools(
1389 const std::string& tools_dir) {
1391 "todo",
"entropic", tools_dir);
1392 todo_ = std::make_unique<TodoTool>(std::move(todo_def));
1396 "complete",
"entropic", tools_dir);
1397 complete_ = std::make_unique<CompleteTool>(
1398 std::move(complete_def));
1401 phase_change_ = std::make_unique<PhaseChangeTool>();
1405 "prune_context",
"entropic", tools_dir);
1406 prune_context_ = std::make_unique<PruneContextTool>(
1407 std::move(prune_def));
1424int EntropicServer::register_delegation_tools(
1425 const std::string& tools_dir,
1426 const std::vector<std::string>& tier_names) {
1427 if (tier_names.size() <= 1) {
1431 "delegate",
"entropic", tools_dir);
1432 delegate_ = std::make_unique<DelegateTool>(
1433 std::move(delegate_def), tier_names);
1437 "pipeline",
"entropic", tools_dir);
1438 pipeline_ = std::make_unique<PipelineTool>(
1439 std::move(pipeline_def), tier_names);
1445 "resume_delegation",
"entropic", tools_dir);
1446 resume_delegation_ = std::make_unique<ResumeDelegationTool>(
1447 std::move(resume_def));
1466int EntropicServer::register_introspection_tools(
1467 const std::string& tools_dir) {
1468 diagnose_ = std::make_unique<DiagnoseTool>(
1472 inspect_ = std::make_unique<InspectTool>(
1476 context_inspect_ = std::make_unique<ContextInspectTool>(
1482 followup_ = std::make_unique<FollowupTool>(
1496 const std::vector<std::string>& tier_names,
1497 const std::string& data_dir)
1500 std::string tools_dir = data_dir +
"/tools";
1501 int count = register_core_tools(tools_dir);
1502 count += register_delegation_tools(tools_dir, tier_names);
1503 count += register_introspection_tools(tools_dir);
1505 logger->info(
"EntropicServer initialized with {} tools "
1506 "({} tiers)",
count, tier_names.size());
1528 const std::string& tool_name)
const {
1529 return tool_name ==
"delegate" || tool_name ==
"pipeline";
1547 state_provider_ = provider;
1548 diagnose_->set_provider(&state_provider_);
1549 inspect_->set_provider(&state_provider_);
1550 context_inspect_->set_provider(&state_provider_);
1552 followup_->set_provider(&state_provider_);
1554 logger->info(
"State provider set for introspection tools");
Tool for inspecting the current context window contents.
ContextInspectTool(ToolDefinition def)
Construct from tool definition.
ServerResponse execute(const std::string &args_json) override
Return context window contents as a message array.
void set_provider(const entropic_state_provider_t *p)
Set state provider pointer.
MCPAccessLevel required_access_level() const override
Read-only tool requires only READ access.
void set_state_provider(const entropic_state_provider_t &provider)
Set the engine state provider for introspection tools.
~EntropicServer() override
Destructor.
EntropicServer(const std::vector< std::string > &tier_names, const std::string &data_dir)
Construct with tier names and data dir.
bool skip_duplicate_check(const std::string &tool_name) const override
delegate and pipeline skip duplicate check.
Concrete base class for MCP servers (80% logic).
void register_tool(ToolBase *tool)
Register a tool with this server.
Tool for pruning old messages from context.
PruneContextTool(ToolDefinition def)
Construct from tool definition.
ServerResponse execute(const std::string &args_json) override
Execute context pruning.
Directive processing for tool-to-engine communication.
Entropic MCP server — engine-level tools including introspection.
@ ENTROPIC_DIRECTIVE_STOP_PROCESSING
Halt directive processing.
@ ENTROPIC_DIRECTIVE_PRUNE_MESSAGES
Prune old tool results.
@ ENTROPIC_DIRECTIVE_COMPLETE
Mark task complete.
@ ENTROPIC_DIRECTIVE_NOTIFY_PRESENTER
Generic UI notification passthrough.
@ ENTROPIC_DIRECTIVE_PHASE_CHANGE
Switch active inference phase.
@ ENTROPIC_DIRECTIVE_PIPELINE
Multi-stage sequential execution.
@ ENTROPIC_DIRECTIVE_CONTEXT_ANCHOR
Replace context anchor.
@ ENTROPIC_DIRECTIVE_DELEGATE
Route to another identity.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Activate model on GPU (WARM → ACTIVE).
static std::string call_provider(char *(*fn)(void *), void *ud)
Call a state provider callback and wrap result.
ToolDefinition load_tool_definition(const std::string &tool_name, const std::string &server_prefix, const std::string &data_dir)
Load a tool definition from a JSON file.
static std::string inspect_filterable(char *(*fn)(void *), void *ud, const std::string &key, const std::string &label)
Inspect a filterable target (config/identity/tool).
static std::vector< std::string > collect_object_keys(const nlohmann::json &j)
Collect keys from a JSON object into a vector.
@ count
Sentinel — MUST remain last.
static std::string filter_json_by_key(const std::string &json_str, const std::string &key, const std::string &label)
Filter a JSON value by key (object key or array name).
static std::string call_docs_provider(char *(*fn)(const char *, void *), const char *section, void *ud)
Call docs callback with section param.
static std::string call_history_provider(char *(*fn)(int, void *), int max_entries, void *ud)
Call history callback with max_entries param.
static bool dispatch_simple_target(const entropic_state_provider_t &p, const std::string &target, const std::string &key, std::string &result)
Dispatch inspect for simple (non-filterable) targets.
static nlohmann::json build_snapshot(const entropic_state_provider_t &p, bool include_docs, int history_limit)
Build the diagnose snapshot JSON.
MCPAccessLevel
MCP tool access level for per-identity authorization.
@ READ
Read-only operations (e.g., read_file, list_directory)
static std::string dispatch_inspect(const entropic_state_provider_t &p, const std::string &target, const std::string &key)
Dispatch an inspect query to the appropriate provider.
static std::vector< std::string > collect_array_names(const nlohmann::json &j)
Collect "name" fields from a JSON array of objects.
static std::string list_available_keys(const nlohmann::json &j)
List available keys from a JSON value for error messages.
static bool dispatch_filterable_target(const entropic_state_provider_t &p, const std::string &target, const std::string &key, std::string &result)
Try filterable targets (config/identity/tool).
MCPServerBase concrete base class + ServerResponse.
Base directive — all directives carry a type tag.
entropic_directive_type_t type
Discriminant for dispatch.
Structured result from tool execution.
std::string content
Item text.
std::string status
"pending", "in_progress", "done"
Read-only engine state provider for introspection tools.
char *(* get_tools)(void *user_data)
Get available tools as JSON array.
char *(* get_metrics)(void *user_data)
Get engine metrics as JSON.
void * user_data
Opaque user data passed to all callbacks.
char *(* search_delegations)(const char *query, int max_results, void *user_data)
Search prior delegation summaries (gh#32, v2.1.6).
char *(* get_identities)(void *user_data)
Get loaded identities as JSON array.
char *(* get_config)(void *user_data)
Get current engine configuration as JSON.
char *(* get_history)(int max_entries, void *user_data)
Get recent tool call history as JSON array.
char *(* get_docs)(const char *section, void *user_data)
Get bundled documentation as text.
char *(* get_state)(void *user_data)
Get engine state as JSON.
UTF-8 validation + replacement at every system boundary where bytes change ownership.