Skip to content

Commit 6f9f0b1

Browse files
committed
Add lookup MCP tool
1 parent f77cf84 commit 6f9f0b1

File tree

6 files changed

+293
-90
lines changed

6 files changed

+293
-90
lines changed

Dockerfile

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@
55
FROM maven:3.9.9-eclipse-temurin-22-jammy AS build
66
WORKDIR /build
77
COPY . .
8-
RUN mvn clean install -DskipTests
8+
RUN mvn clean install
99

1010
#######################################
1111
# Run stage
1212
FROM eclipse-temurin:22-jre-alpine AS run
1313

1414
# Add labels
15-
LABEL org.opencontainers.image.title="Convex"
16-
LABEL org.opencontainers.image.description="Convex Peer Node"
17-
LABEL org.opencontainers.image.source="https://github.com/Convex-Dev/convex"
18-
LABEL org.opencontainers.image.source="https://convex.world"
15+
LABEL
16+
org.opencontainers.image.title="Convex" \
17+
org.opencontainers.image.description="Convex Peer Node" \
18+
org.opencontainers.image.source="https://github.com/Convex-Dev/convex" \
19+
org.opencontainers.image.source="https://convex.world"
1920

2021
# Create non-root user
2122
RUN addgroup -S convex && adduser -S convex -G convex

convex-core/src/main/java/convex/core/data/Strings.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ public class Strings {
9090

9191
public static final StringShort CAD3 = StringShort.intern("cad3");
9292

93+
public static final StringShort SYMBOL = StringShort.intern("symbol");
94+
9395
public static final Comparator<AString> lengthComparator = (a,b)->{
9496
return Long.signum(a.count()-b.count());
9597
};

convex-core/src/main/java/convex/core/data/Symbol.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,7 @@ public static Symbol create(String name) {
5555
*/
5656
public static Symbol create(AString name) {
5757
if (!validateName(name)) return null;
58-
5958
Symbol sym= new Symbol((StringShort)name);
60-
61-
6259
return sym;
6360
}
6461

convex-restapi/src/main/java/convex/restapi/api/McpAPI.java

Lines changed: 101 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,12 @@ private AMap<AString, ACell> listTools() {
263263
return Maps.of(Strings.create("tools"), listToolsVector());
264264
}
265265

266+
/* JSON-RPC protocol result */
266267
private AMap<AString, ACell> protocolResult(AMap<AString, ACell> result) {
267268
return BASE_RESPONSE.assoc(FIELD_RESULT, result);
268269
}
269270

271+
/* JSON-RPC protocol error */
270272
private AMap<AString, ACell> protocolError(int code, String message) {
271273
AMap<AString, ACell> error = Maps.of(
272274
FIELD_CODE, CVMLong.create(code),
@@ -295,9 +297,16 @@ private AMap<AString, ACell> toolCall(ACell paramsCell) throws InterruptedExcept
295297
return protocolError(-32601, "Unknown tool: " + toolName);
296298
}
297299

298-
return tool.handle(RT.ensureMap(params.get(FIELD_ARGUMENTS)));
300+
AMap<AString,ACell> arguments=RT.ensureMap(params.get(FIELD_ARGUMENTS));
301+
302+
if (arguments == null) {
303+
return protocolError(-32602, toolName +" requires arguments");
304+
}
305+
306+
return tool.handle(arguments);
299307
}
300308

309+
/* Create a Result from a CVM Result */
301310
private AMap<AString, ACell> toolResult(Result result) {
302311
AMap<AString, ACell> structured = EMPTY_MAP;
303312
ACell value = result.getValue();
@@ -321,6 +330,7 @@ private AMap<AString, ACell> toolSuccess(ACell structuredResult) {
321330
return protocolResult(buildMcpResult(payload, false));
322331
}
323332

333+
/* Create an error result for a tool call (but protocol valid) */
324334
private AMap<AString, ACell> toolError(String message) {
325335
AMap<AString, ACell> payload = Maps.of(
326336
Strings.create("message"), Strings.create(message)
@@ -356,6 +366,7 @@ private void registerTools() {
356366
registerTool(new ValidateTool());
357367
registerTool(new CreateAccountTool());
358368
registerTool(new DescribeAccountTool());
369+
registerTool(new LookupTool());
359370
}
360371

361372
private void registerTool(McpTool tool) {
@@ -378,9 +389,6 @@ private class QueryTool extends McpTool {
378389

379390
@Override
380391
public AMap<AString, ACell> handle(AMap<AString, ACell> arguments) throws InterruptedException {
381-
if (arguments == null) {
382-
return protocolError(-32602, "Query requires arguments");
383-
}
384392
AString sourceCell = RT.ensureString(arguments.get(ARG_SOURCE));
385393
if (sourceCell == null) {
386394
return protocolError(-32602, "Query requires 'source' string");
@@ -412,9 +420,6 @@ private class TransactTool extends McpTool {
412420

413421
@Override
414422
public AMap<AString, ACell> handle(AMap<AString, ACell> arguments) throws InterruptedException {
415-
if (arguments == null) {
416-
return protocolError(-32602, "Transact requires arguments");
417-
}
418423
AString sourceCell = RT.ensureString(arguments.get(ARG_SOURCE));
419424
if (sourceCell == null) {
420425
return protocolError(-32602, "Transact requires 'source' string");
@@ -519,9 +524,6 @@ private class HashTool extends McpTool {
519524

520525
@Override
521526
public AMap<AString, ACell> handle(AMap<AString, ACell> arguments) {
522-
if (arguments == null) {
523-
return protocolError(-32602, "Hash tool requires arguments");
524-
}
525527
AString valueCell = RT.ensureString(arguments.get(ARG_VALUE));
526528
if (valueCell == null) {
527529
return protocolError(-32602, "Hash tool requires 'value' string");
@@ -554,9 +556,6 @@ private class SignTool extends McpTool {
554556

555557
@Override
556558
public AMap<AString, ACell> handle(AMap<AString, ACell> arguments) {
557-
if (arguments == null) {
558-
return protocolError(-32602, "Sign tool requires arguments");
559-
}
560559
AString valueCell = RT.ensureString(arguments.get(ARG_VALUE));
561560
if (valueCell == null) {
562561
return toolError("Sign tool requires a 'value' hex string");
@@ -597,9 +596,6 @@ private class SubmitTool extends McpTool {
597596

598597
@Override
599598
public AMap<AString, ACell> handle(AMap<AString, ACell> arguments) throws InterruptedException {
600-
if (arguments == null) {
601-
return protocolError(-32602, "Submit requires arguments");
602-
}
603599
AString hashCell = RT.ensureString(arguments.get(Strings.create("hash")));
604600
if (hashCell == null) {
605601
return protocolError(-32602, "Submit requires 'hash' string");
@@ -660,9 +656,6 @@ private class EncodeTool extends McpTool {
660656

661657
@Override
662658
public AMap<AString, ACell> handle(AMap<AString, ACell> arguments) {
663-
if (arguments == null) {
664-
return protocolError(-32602, "Encode requires arguments");
665-
}
666659
AString cvxCell = RT.ensureString(arguments.get(Strings.create("cvx")));
667660
if (cvxCell == null) {
668661
return protocolError(-32602, "Encode requires 'cvx' string");
@@ -688,9 +681,6 @@ private class DecodeTool extends McpTool {
688681

689682
@Override
690683
public AMap<AString, ACell> handle(AMap<AString, ACell> arguments) {
691-
if (arguments == null) {
692-
return protocolError(-32602, "Decode requires arguments");
693-
}
694684
AString cad3Cell = RT.ensureString(arguments.get(Strings.create("cad3")));
695685
if (cad3Cell == null) {
696686
return protocolError(-32602, "Decode requires 'cad3' string");
@@ -758,9 +748,6 @@ private class ValidateTool extends McpTool {
758748

759749
@Override
760750
public AMap<AString, ACell> handle(AMap<AString, ACell> arguments) {
761-
if (arguments == null) {
762-
return protocolError(-32602, "Validate requires arguments");
763-
}
764751
AString publicKeyCell = RT.ensureString(arguments.get(ARG_PUBLIC_KEY));
765752
if (publicKeyCell == null) {
766753
return protocolError(-32602, "Validate requires 'publicKey' string");
@@ -832,9 +819,6 @@ private class CreateAccountTool extends McpTool {
832819

833820
@Override
834821
public AMap<AString, ACell> handle(AMap<AString, ACell> arguments) throws InterruptedException {
835-
if (arguments == null) {
836-
return protocolError(-32602, "CreateAccount requires arguments");
837-
}
838822
AString accountKeyCell = RT.ensureString(arguments.get(ARG_ACCOUNT_KEY));
839823
if (accountKeyCell == null) {
840824
return protocolError(-32602, "CreateAccount requires 'accountKey' string");
@@ -932,6 +916,95 @@ public AMap<AString, ACell> handle(AMap<AString, ACell> arguments) throws Interr
932916
}
933917
}
934918

919+
private class LookupTool extends McpTool {
920+
LookupTool() {
921+
super(McpTool.loadMetadata("convex/restapi/mcp/tools/lookup.json"));
922+
}
923+
924+
@Override
925+
public AMap<AString, ACell> handle(AMap<AString, ACell> arguments) throws InterruptedException {
926+
try {
927+
// Parse address
928+
ACell addressCell = arguments.get(ARG_ADDRESS);
929+
if (addressCell == null) {
930+
return toolError("Lookup requires 'address' parameter, e.g. '#5675' or '@convex.core'");
931+
}
932+
Address address = Address.parse(addressCell);
933+
if (address == null) {
934+
return toolError("Invalid address format");
935+
}
936+
937+
// Parse symbol
938+
AString symbolCell = RT.ensureString(arguments.get(Strings.SYMBOL));
939+
if (symbolCell == null) {
940+
return toolError("Lookup requires 'symbol' parameter");
941+
}
942+
Symbol symbol = RT.ensureSymbol(Reader.read(symbolCell));
943+
944+
// Get account status
945+
AccountStatus accountStatus = server.getPeer().getConsensusState().getAccount(address);
946+
if (accountStatus == null) {
947+
return toolError("Account not found: " + address);
948+
}
949+
950+
// Check if symbol exists in environment
951+
AHashMap<Symbol, ACell> env = accountStatus.getEnvironment();
952+
boolean exists = (env != null) && env.containsKey(symbol);
953+
954+
// Get the value
955+
ACell value = null;
956+
if (exists && env != null) {
957+
value = env.get(symbol);
958+
959+
// Apply path if provided
960+
AString pathCell = RT.ensureString(arguments.get(Strings.create("getPath")));
961+
if (pathCell != null && value != null) {
962+
String pathStr = pathCell.toString();
963+
try {
964+
// Parse the path as a sequence
965+
ACell pathForm = Reader.read(pathStr);
966+
AVector<ACell> pathSeq = RT.ensureVector(pathForm);
967+
if (pathSeq != null) {
968+
// Convert sequence to array for RT.getIn
969+
long pathLen = pathSeq.count();
970+
ACell[] pathKeys = new ACell[(int)pathLen];
971+
for (long i = 0; i < pathLen; i++) {
972+
pathKeys[(int)i] = pathSeq.get(i);
973+
}
974+
value = RT.getIn(value, pathKeys);
975+
} else {
976+
// If not a vector, try as a single key
977+
value = RT.getIn(value, pathForm);
978+
}
979+
} catch (Exception e) {
980+
return toolError("Failed to parse getPath: " + e.getMessage());
981+
}
982+
}
983+
}
984+
985+
// Get metadata for the symbol (only if it exists)
986+
AHashMap<ACell, ACell> meta = null;
987+
if (exists) {
988+
AHashMap<ACell, ACell> symbolMeta = accountStatus.getMetadata(symbol);
989+
// Return metadata only if it's not empty
990+
if (symbolMeta != null && !symbolMeta.isEmpty()) {
991+
meta = symbolMeta;
992+
}
993+
}
994+
995+
// Build result
996+
AMap<AString, ACell> resultMap = Maps.of(
997+
Strings.create("exists"), exists ? CVMBool.TRUE : CVMBool.FALSE,
998+
Strings.create("value"), value != null ? value : RT.cvm(null),
999+
Strings.create("meta"), meta != null ? meta : RT.cvm(null)
1000+
);
1001+
return toolSuccess(resultMap);
1002+
} catch (Exception e) {
1003+
return toolError("Lookup failed: " + e.getMessage());
1004+
}
1005+
}
1006+
}
1007+
9351008
private AMap<AString,ACell> WELL_KNOWN=JSON.parse("""
9361009
{
9371010
"mcp_version": "1.0",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "lookup",
3+
"title": "Symbol Lookup",
4+
"description": "Look up a symbolic value in an account. Returns whether the symbol exists, its value (optionally using a path), and its metadata.",
5+
"inputSchema": {
6+
"type": "object",
7+
"properties": {
8+
"address": {
9+
"type": "string",
10+
"description": "Address of the account (e.g. '#5675' or '@convex.core')"
11+
},
12+
"symbol": {
13+
"type": "string",
14+
"description": "Symbol name to look up at specified address (e.g. 'user-database')"
15+
},
16+
"getPath": {
17+
"type": "string",
18+
"description": "Optional path to navigate into the value (e.g. '[:users 7 :name]') in CVX format"
19+
}
20+
},
21+
"required": ["address", "symbol"]
22+
},
23+
"outputSchema": {
24+
"type": "object",
25+
"properties": {
26+
"address": {
27+
"type": "string",
28+
"description": "Resolved address of the account (e.g. '#5675')"
29+
},
30+
31+
"exists": {
32+
"type": "boolean",
33+
"description": "Whether the symbol exists in the account environment"
34+
},
35+
"value": {
36+
"type": "string",
37+
"description": "The value at the symbol (using getIn with the path if provided) in CVX format"
38+
},
39+
"meta": {
40+
"type": "string",
41+
"description": "The metadata on the symbol (can be null) in CVX format"
42+
}
43+
}
44+
}
45+
}
46+

0 commit comments

Comments
 (0)