Skip to content

Commit e7be61b

Browse files
author
Nicola Spieser
committed
feat: server instructions, custom version, tool enable/disable — bump to v0.30.0
- Add setInstructions() for MCP 2025-03-26 'instructions' field in initialize response - Add setVersion() to override server version string in server info - Add enableTool()/disableTool()/isToolEnabled() for runtime tool visibility control - Disabled tools are hidden from tools/list and rejected on tools/call - Add 9 integration tests for all new features - Total: 1037 tests, all passing
1 parent 3f0b5dc commit e7be61b

6 files changed

Lines changed: 204 additions & 7 deletions

File tree

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
|---|:---:|:---:|:---:|
3434
| Runs on the MCU ||| ❌ CLI tool |
3535
| MCP spec compliant | ✅ 2025-03-26 | ❌ custom WS ||
36-
| Actually compiles |1028 tests | ❌ self-described | N/A |
36+
| Actually compiles |1037 tests | ❌ self-described | N/A |
3737
| Streamable HTTP + SSE ||||
3838
| WebSocket transport ||||
3939
| Claude Desktop bridge ||||
@@ -44,6 +44,8 @@
4444
| Elicitation (server requests user input) ||||
4545
| Audio content type ||||
4646
| Tool Annotations (readOnly, destructive hints) ||||
47+
| Server Instructions (LLM guidance) ||||
48+
| Runtime Tool Enable/Disable ||||
4749
| Structured Content (text, image, resource) ||||
4850
| Progress Notifications ||||
4951
| Request Cancellation ||||
@@ -314,6 +316,31 @@ mcp.notifyToolsChanged(); // Notify connected clients
314316

315317
mcp.removeTool("old_sensor");
316318
mcp.notifyToolsChanged();
319+
320+
// Enable/disable tools without removing them
321+
mcp.disableTool("maintenance_tool"); // Hidden from clients
322+
mcp.enableTool("maintenance_tool"); // Visible again
323+
```
324+
325+
### 📋 Server Instructions
326+
327+
Guide the LLM's behavior with your device using server instructions (MCP 2025-03-26):
328+
329+
```cpp
330+
mcp.setInstructions(
331+
"This device controls a greenhouse. Always read temperature "
332+
"before adjusting fans. Never set irrigation above 80%."
333+
);
334+
```
335+
336+
Instructions are sent in the `initialize` response and help the model understand the context and constraints of your hardware.
337+
338+
### 🏷️ Custom Version
339+
340+
Override the version string in server info (useful for firmware versioning):
341+
342+
```cpp
343+
mcp.setVersion("2.1.0-greenhouse");
317344
```
318345

319346
### 📊 Prometheus Metrics

src/mcpd.cpp

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,27 @@ void Server::addRoot(const char* uri, const char* name) {
8282
void Server::setEndpoint(const char* path) { _endpoint = path; }
8383
void Server::setMDNS(bool enabled) { _mdnsEnabled = enabled; }
8484

85+
bool Server::enableTool(const char* name, bool enabled) {
86+
// Verify tool exists
87+
bool found = false;
88+
for (const auto& t : _tools) {
89+
if (t.name == name) { found = true; break; }
90+
}
91+
if (!found) return false;
92+
93+
if (enabled) {
94+
_disabledTools.erase(String(name));
95+
} else {
96+
_disabledTools.insert(String(name));
97+
}
98+
notifyToolsChanged();
99+
return true;
100+
}
101+
102+
bool Server::isToolEnabled(const char* name) const {
103+
return _disabledTools.find(String(name)) == _disabledTools.end();
104+
}
105+
85106
bool Server::removeTool(const char* name) {
86107
for (auto it = _tools.begin(); it != _tools.end(); ++it) {
87108
if (it->name == name) {
@@ -586,7 +607,12 @@ String Server::_handleInitialize(JsonVariant params, JsonVariant id) {
586607

587608
JsonObject serverInfo = result["serverInfo"].to<JsonObject>();
588609
serverInfo["name"] = _name;
589-
serverInfo["version"] = MCPD_VERSION;
610+
serverInfo["version"] = _version ? _version : MCPD_VERSION;
611+
612+
// MCP 2025-03-26: instructions guide the LLM's behavior with this server
613+
if (_instructions) {
614+
result["instructions"] = _instructions;
615+
}
590616

591617
JsonObject capabilities = result["capabilities"].to<JsonObject>();
592618

@@ -663,6 +689,8 @@ String Server::_handleToolsList(JsonVariant params, JsonVariant id) {
663689
}
664690

665691
for (size_t i = startIdx; i < endIdx; i++) {
692+
// Skip disabled tools
693+
if (_disabledTools.count(_tools[i].name)) continue;
666694
JsonObject obj = tools.add<JsonObject>();
667695
_tools[i].toJson(obj);
668696
}
@@ -698,6 +726,14 @@ String Server::_handleToolsCall(JsonVariant params, JsonVariant id) {
698726
_requestTracker.trackRequest(requestId, progressToken);
699727
}
700728

729+
// Reject disabled tools
730+
if (_disabledTools.count(String(toolName))) {
731+
if (!requestId.isEmpty()) {
732+
_requestTracker.completeRequest(requestId);
733+
}
734+
return _jsonRpcError(id, -32602, "Tool not found");
735+
}
736+
701737
// Find the tool
702738
for (const auto& tool : _tools) {
703739
if (tool.name == toolName) {

src/mcpd.h

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
#include "MCPTransportBLE.h"
4545
#endif
4646

47-
#define MCPD_VERSION "0.29.1"
47+
#define MCPD_VERSION "0.30.0"
4848
#define MCPD_MCP_PROTOCOL_VERSION "2025-03-26"
4949

5050
namespace mcpd {
@@ -155,6 +155,28 @@ class Server {
155155
/** Enable/disable mDNS advertisement (default: enabled) */
156156
void setMDNS(bool enabled);
157157

158+
/**
159+
* Set server instructions for the LLM (MCP 2025-03-26 spec).
160+
* Included in the initialize response to guide the model's behavior.
161+
* Example: "This device controls a greenhouse. Use temperature_read before adjusting fans."
162+
*/
163+
void setInstructions(const char* instructions) { _instructions = instructions; }
164+
165+
/**
166+
* Set a custom version string (overrides MCPD_VERSION in server info).
167+
*/
168+
void setVersion(const char* version) { _version = version; }
169+
170+
/**
171+
* Enable or disable a tool at runtime by name.
172+
* Disabled tools are hidden from tools/list and cannot be called.
173+
* Emits tools/list_changed notification.
174+
* @return true if the tool was found
175+
*/
176+
bool enableTool(const char* name, bool enabled = true);
177+
bool disableTool(const char* name) { return enableTool(name, false); }
178+
bool isToolEnabled(const char* name) const;
179+
158180
/** Get server name */
159181
const char* getName() const { return _name; }
160182

@@ -377,7 +399,10 @@ class Server {
377399
const char* _name;
378400
uint16_t _port;
379401
const char* _endpoint = "/mcp";
402+
const char* _instructions = nullptr;
403+
const char* _version = nullptr;
380404
bool _mdnsEnabled = true;
405+
std::set<String> _disabledTools; // tools hidden from listing/calling
381406
String _sessionId;
382407
bool _initialized = false;
383408
size_t _pageSize = 0; // 0 = no pagination

test/test_infrastructure.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,7 @@ TEST(completion_manager_no_providers) {
719719
// ═══════════════════════════════════════════════════════════════════════
720720

721721
TEST(version_constants) {
722-
ASSERT_EQ(String(MCPD_VERSION), String("0.29.1"));
722+
ASSERT_EQ(String(MCPD_VERSION), String("0.30.0"));
723723
ASSERT_EQ(String(MCPD_MCP_PROTOCOL_VERSION), String("2025-03-26"));
724724
}
725725

test/test_integration.cpp

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,115 @@ TEST(error_with_data_preserves_id) {
779779
ASSERT(err.indexOf("field_x") >= 0);
780780
}
781781

782+
// ════════════════════════════════════════════════════════════════════════
783+
// Instructions & Version Tests
784+
// ════════════════════════════════════════════════════════════════════════
785+
786+
TEST(instructions_included_in_initialize) {
787+
Server server("test", 80);
788+
server.setInstructions("Read temperature before adjusting fans.");
789+
790+
String res = server._processJsonRpc(
791+
R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test"}}})");
792+
ASSERT(res.indexOf("Read temperature before adjusting fans.") >= 0);
793+
ASSERT(res.indexOf("\"instructions\"") >= 0);
794+
}
795+
796+
TEST(instructions_absent_when_not_set) {
797+
Server server("test", 80);
798+
799+
String res = server._processJsonRpc(
800+
R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test"}}})");
801+
ASSERT(res.indexOf("\"instructions\"") < 0);
802+
}
803+
804+
TEST(custom_version_in_initialize) {
805+
Server server("test", 80);
806+
server.setVersion("1.2.3-custom");
807+
808+
String res = server._processJsonRpc(
809+
R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test"}}})");
810+
ASSERT(res.indexOf("1.2.3-custom") >= 0);
811+
}
812+
813+
TEST(default_version_when_not_overridden) {
814+
Server server("test", 80);
815+
816+
String res = server._processJsonRpc(
817+
R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test"}}})");
818+
ASSERT(res.indexOf(MCPD_VERSION) >= 0);
819+
}
820+
821+
// ════════════════════════════════════════════════════════════════════════
822+
// Tool Enable/Disable Tests
823+
// ════════════════════════════════════════════════════════════════════════
824+
825+
TEST(disable_tool_hides_from_list) {
826+
Server server("test", 80);
827+
server.addTool("visible", "A tool", "{}", [](const JsonObject&) { return String("ok"); });
828+
server.addTool("hidden", "Another", "{}", [](const JsonObject&) { return String("ok"); });
829+
830+
server._processJsonRpc(
831+
R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test"}}})");
832+
833+
// Both visible initially
834+
String res = server._processJsonRpc(
835+
R"({"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}})");
836+
ASSERT(res.indexOf("\"visible\"") >= 0);
837+
ASSERT(res.indexOf("\"hidden\"") >= 0);
838+
839+
// Disable one
840+
ASSERT(server.disableTool("hidden"));
841+
res = server._processJsonRpc(
842+
R"({"jsonrpc":"2.0","id":3,"method":"tools/list","params":{}})");
843+
ASSERT(res.indexOf("\"visible\"") >= 0);
844+
ASSERT(res.indexOf("\"hidden\"") < 0);
845+
}
846+
847+
TEST(disabled_tool_cannot_be_called) {
848+
Server server("test", 80);
849+
server.addTool("mytool", "A tool", "{}", [](const JsonObject&) { return String("ok"); });
850+
851+
server._processJsonRpc(
852+
R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test"}}})");
853+
854+
server.disableTool("mytool");
855+
String res = server._processJsonRpc(
856+
R"({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mytool","arguments":{}}})");
857+
ASSERT(res.indexOf("\"error\"") >= 0);
858+
ASSERT(res.indexOf("Tool not found") >= 0);
859+
}
860+
861+
TEST(re_enable_tool_makes_it_visible) {
862+
Server server("test", 80);
863+
server.addTool("toggle", "Togglable", "{}", [](const JsonObject&) { return String("ok"); });
864+
865+
server._processJsonRpc(
866+
R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test"}}})");
867+
868+
server.disableTool("toggle");
869+
ASSERT(server.isToolEnabled("toggle") == false);
870+
871+
server.enableTool("toggle");
872+
ASSERT(server.isToolEnabled("toggle") == true);
873+
874+
String res = server._processJsonRpc(
875+
R"({"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}})");
876+
ASSERT(res.indexOf("\"toggle\"") >= 0);
877+
}
878+
879+
TEST(enable_nonexistent_tool_returns_false) {
880+
Server server("test", 80);
881+
ASSERT(server.enableTool("nonexistent") == false);
882+
ASSERT(server.disableTool("nonexistent") == false);
883+
}
884+
885+
TEST(is_tool_enabled_default_true) {
886+
Server server("test", 80);
887+
server.addTool("mytool", "test", "{}", [](const JsonObject&) { return String("ok"); });
888+
ASSERT(server.isToolEnabled("mytool") == true);
889+
}
890+
782891
// ════════════════════════════════════════════════════════════════════════
783892

784893
int main() {

test/test_jsonrpc.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,7 @@ TEST(version_is_0_11_0_compat) {
712712
auto* s = makeTestServer();
713713
String req = R"({"jsonrpc":"2.0","id":250,"method":"initialize","params":{}})";
714714
String resp = s->_processJsonRpc(req);
715-
ASSERT_STR_CONTAINS(resp.c_str(), "\"version\":\"0.29.1\"");
715+
ASSERT_STR_CONTAINS(resp.c_str(), "\"version\":\"0.30.0\"");
716716
}
717717

718718
// ── v0.6.0 Tests: Tool Annotations ────────────────────────────────────
@@ -1567,7 +1567,7 @@ TEST(version_0_11_0) {
15671567
Server* s = makeTestServer();
15681568
String req = R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test"}}})";
15691569
String resp = s->_processJsonRpc(req);
1570-
ASSERT_STR_CONTAINS(resp.c_str(), "0.29.1");
1570+
ASSERT_STR_CONTAINS(resp.c_str(), "0.30.0");
15711571
}
15721572

15731573
// ── Watchdog Tool Tests ────────────────────────────────────────────────
@@ -1811,7 +1811,7 @@ TEST(diagnostics_version_macros) {
18111811
ASSERT(strlen(MCPD_VERSION) > 0);
18121812
ASSERT(strlen(MCPD_MCP_PROTOCOL_VERSION) > 0);
18131813
ASSERT_STR_CONTAINS(MCPD_MCP_PROTOCOL_VERSION, "2025");
1814-
ASSERT_STR_CONTAINS(MCPD_VERSION, "0.29.1");
1814+
ASSERT_STR_CONTAINS(MCPD_VERSION, "0.30.0");
18151815
}
18161816

18171817
// ── Batch JSON-RPC edge cases ──────────────────────────────────────────

0 commit comments

Comments
 (0)