Skip to content

Commit a57ed1b

Browse files
committed
Add get_values tool to MCP server, register 13 tools
- Implement `get_values` tool for retrieving current values from cache or InfluxDB - Add `get_values` to MCP server, updating tool count to 13 - Introduce supporting methods in I3xRag for value retrieval - Update tests to cover `get_values` functionality and tool registration
1 parent d9eaa6f commit a57ed1b

4 files changed

Lines changed: 125 additions & 2 deletions

File tree

acs-i3x/bin/api.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/*
2+
* Copyright (c) University of Sheffield AMRC 2026.
3+
*/
4+
15
import { ServiceClient, UUIDs } from "@amrc-factoryplus/service-client";
26
import { WebAPI } from "@amrc-factoryplus/service-api";
37
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -70,7 +74,7 @@ logger.debug({ nodes: i3xRag.nodeCount(), edges: i3xRag.edgeCount() }, "RAG engi
7074
// MCP server
7175
const mcpServer = new McpServer({ name: "acs-i3x-rag", version: "1.0.0" });
7276
registerRagTools(mcpServer, i3xRag);
73-
logger.info("MCP server registered with 12 RAG tools");
77+
logger.info("MCP server registered with 13 RAG tools");
7478

7579
// Start HTTP server via WebAPI
7680
// In dev mode, make all paths public (no Kerberos keytab needed)

acs-i3x/src/mcp/tools.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/*
2+
* Copyright (c) University of Sheffield AMRC 2026.
3+
*/
4+
15
/*
26
* MCP tool registrations for I3xRag.
37
*
@@ -149,4 +153,14 @@ export function registerRagTools(server: McpServer, rag: I3xRag): void {
149153
return jsonResult(history);
150154
},
151155
);
156+
157+
server.tool(
158+
"get_values",
159+
"Get current values for one or more elements, trying cache first then InfluxDB.",
160+
{ element_ids: z.array(z.string()) },
161+
async ({ element_ids }) => {
162+
const values = await rag.getValues(element_ids);
163+
return jsonResult(values);
164+
},
165+
);
152166
}

acs-i3x/src/rag/i3x-rag.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/*
2+
* Copyright (c) University of Sheffield AMRC 2026.
3+
*/
4+
15
/*
26
* I3xRag — graph + search index for RAG-style queries over the i3X object tree.
37
*
@@ -44,6 +48,8 @@ export interface HistoryLike {
4448
startTime: string,
4549
endTime: string,
4650
): Promise<I3xVqt[]>;
51+
getCurrentValue(elementId: string): Promise<{ elementId: string; isComposition: boolean; value: unknown; quality: string; timestamp: string } | null>;
52+
getCompositionValue(elementId: string, maxDepth?: number): Promise<{ elementId: string; isComposition: boolean; value: unknown; quality: string; timestamp: string } | null>;
4753
}
4854

4955
/* ------------------------------------------------------------------ */
@@ -106,6 +112,15 @@ export interface ValueFilterResult {
106112
timestamp: string;
107113
}
108114

115+
export interface CurrentValueResult {
116+
elementId: string;
117+
displayName: string;
118+
value: unknown;
119+
quality: string;
120+
timestamp: string;
121+
source: "cache" | "influxdb";
122+
}
123+
109124
/* ------------------------------------------------------------------ */
110125
/* I3xRag */
111126
/* ------------------------------------------------------------------ */
@@ -356,6 +371,79 @@ export class I3xRag {
356371
return this.history.queryHistory(elementId, startTime, endTime);
357372
}
358373

374+
/** Get current values for multiple elements, trying cache first then InfluxDB. */
375+
async getValues(elementIds: string[]): Promise<CurrentValueResult[]> {
376+
const results: CurrentValueResult[] = [];
377+
378+
for (const elementId of elementIds) {
379+
const attrs = this.graph.getNodeAttributes(elementId);
380+
if (!attrs) {
381+
results.push({
382+
elementId,
383+
displayName: "Unknown",
384+
value: undefined,
385+
quality: "Unknown",
386+
timestamp: "",
387+
source: "cache",
388+
});
389+
continue;
390+
}
391+
392+
// Try cache first
393+
const cached = this.valueCache.getValue(elementId);
394+
if (cached) {
395+
results.push({
396+
elementId,
397+
displayName: attrs.displayName,
398+
value: cached.value,
399+
quality: cached.quality,
400+
timestamp: cached.timestamp,
401+
source: "cache",
402+
});
403+
continue;
404+
}
405+
406+
// Fall back to InfluxDB
407+
try {
408+
const obj = this.objectTree.getObject(elementId);
409+
const influxValue = obj?.isComposition
410+
? await this.history.getCompositionValue(elementId)
411+
: await this.history.getCurrentValue(elementId);
412+
413+
if (influxValue) {
414+
results.push({
415+
elementId,
416+
displayName: attrs.displayName,
417+
value: influxValue.value,
418+
quality: influxValue.quality,
419+
timestamp: influxValue.timestamp,
420+
source: "influxdb",
421+
});
422+
} else {
423+
results.push({
424+
elementId,
425+
displayName: attrs.displayName,
426+
value: undefined,
427+
quality: "Unknown",
428+
timestamp: "",
429+
source: "influxdb",
430+
});
431+
}
432+
} catch (err) {
433+
results.push({
434+
elementId,
435+
displayName: attrs.displayName,
436+
value: undefined,
437+
quality: "Error",
438+
timestamp: "",
439+
source: "influxdb",
440+
});
441+
}
442+
}
443+
444+
return results;
445+
}
446+
359447
/* -- internals ------------------------------------------------- */
360448

361449
private buildCompositionNode(

acs-i3x/test/mcp/tools.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/*
2+
* Copyright (c) University of Sheffield AMRC 2026.
3+
*/
4+
15
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
26
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
37
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -44,13 +48,14 @@ describe("MCP tool registrations", () => {
4448
client = harness.client;
4549
});
4650

47-
it("lists all 12 tools", async () => {
51+
it("lists all 13 tools", async () => {
4852
const { tools } = await client.listTools();
4953
const names = tools.map(t => t.name).sort();
5054
expect(names).toEqual([
5155
"composition_tree",
5256
"find_path",
5357
"get_history",
58+
"get_values",
5459
"neighborhood",
5560
"relationship_map",
5661
"search",
@@ -109,6 +114,18 @@ describe("MCP tool registrations", () => {
109114
expect(data[0].elementId).toBe("joint1-angle");
110115
});
111116

117+
it("get_values retrieves current values from cache for multiple elements", async () => {
118+
const result = await client.callTool({ name: "get_values", arguments: { element_ids: ["x-pos", "y-pos", "joint1-angle"] } });
119+
const data = parseResult(result);
120+
expect(Array.isArray(data)).toBe(true);
121+
expect(data.length).toBe(3);
122+
const xPos = data.find((v: any) => v.elementId === "x-pos");
123+
expect(xPos).toBeDefined();
124+
expect(xPos.value).toBe(123.4);
125+
expect(xPos.quality).toBe("Good");
126+
expect(xPos.source).toBe("cache");
127+
});
128+
112129
it("search with missing required query param returns error", async () => {
113130
const result = await client.callTool({ name: "search", arguments: {} });
114131
expect(result.isError).toBe(true);

0 commit comments

Comments
 (0)