Skip to content

Commit 25611e9

Browse files
committed
feat(assets): list assets tool
1 parent 300d0e6 commit 25611e9

File tree

8 files changed

+370
-4
lines changed

8 files changed

+370
-4
lines changed

doit-mcp-server/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ import {
5555
listAllocationsTool,
5656
getAllocationTool,
5757
} from "../../src/tools/allocations.js";
58+
import {
59+
ListAssetsArgumentsSchema,
60+
listAssetsTool,
61+
} from "../../src/tools/assets.js";
5862
import {
5963
ChangeCustomerArgumentsSchema,
6064
changeCustomerTool,
@@ -260,6 +264,9 @@ export class DoitMCPAgent extends McpAgent {
260264
this.registerTool(listAllocationsTool, ListAllocationsArgumentsSchema);
261265
this.registerTool(getAllocationTool, GetAllocationArgumentsSchema);
262266

267+
// Assets tools
268+
this.registerTool(listAssetsTool, ListAssetsArgumentsSchema);
269+
263270
// Change Customer tool (requires special handling)
264271
if (this.props.isDoitUser === "true") {
265272
(this.server.tool as any)(

src/__tests__/index.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ vi.mock("../tools/allocations.js", () => ({
9393
handleListAllocationsRequest: vi.fn(),
9494
handleGetAllocationRequest: vi.fn(),
9595
}));
96+
97+
vi.mock("../tools/assets.js", () => ({
98+
listAssetsTool: {
99+
name: "list_assets",
100+
description:
101+
"Returns a list of all available customer assets such as Google Cloud billing accounts, G Suite/Workspace subscriptions, etc. Assets are returned in reverse chronological order by default.",
102+
},
103+
handleListAssetsRequest: vi.fn(),
104+
}));
96105
vi.mock("../utils/util.js", async () => {
97106
const actual = await vi.importActual("../utils/util.js");
98107
return {
@@ -246,6 +255,11 @@ describe("ListToolsRequestSchema Handler", () => {
246255
name: "get_allocation",
247256
description: "Get a specific allocation by ID from the DoiT API",
248257
},
258+
{
259+
name: "list_assets",
260+
description:
261+
"Returns a list of all available customer assets such as Google Cloud billing accounts, G Suite/Workspace subscriptions, etc. Assets are returned in reverse chronological order by default.",
262+
},
249263
],
250264
});
251265
});
@@ -539,6 +553,18 @@ describe("CallToolRequestSchema Handler", () => {
539553
expect(handleGetAllocationRequest).toHaveBeenCalledWith(args, "fake-token");
540554
});
541555

556+
it("should route to the correct tool handler for list_assets", async () => {
557+
const callToolHandler = setRequestHandlerMock.mock.calls.find(
558+
(call) => call[0] === CallToolRequestSchema
559+
)?.[1];
560+
const args = { pageToken: "next-page" };
561+
const request = mockRequest("list_assets", args);
562+
563+
await callToolHandler(request);
564+
565+
expect(handleListAssetsRequest).toHaveBeenCalledWith(args, "fake-token");
566+
});
567+
542568
it("should return Unknown tool error for unknown tool names", async () => {
543569
const callToolHandler = setRequestHandlerMock.mock.calls.find(
544570
(call) => call[0] === CallToolRequestSchema
@@ -644,4 +670,5 @@ const {
644670
handleGetInvoiceRequest,
645671
handleListAllocationsRequest,
646672
handleGetAllocationRequest,
673+
handleListAssetsRequest,
647674
} = indexModule;

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
getAllocationTool,
6464
handleGetAllocationRequest,
6565
} from "./tools/allocations.js";
66+
import { listAssetsTool, handleListAssetsRequest } from "./tools/assets.js";
6667

6768
dotenv.config();
6869

@@ -101,6 +102,7 @@ function createServer() {
101102
getInvoiceTool,
102103
listAllocationsTool,
103104
getAllocationTool,
105+
listAssetsTool,
104106
],
105107
};
106108
});
@@ -194,4 +196,5 @@ export {
194196
handleGetInvoiceRequest,
195197
handleListAllocationsRequest,
196198
handleGetAllocationRequest,
199+
handleListAssetsRequest,
197200
};

src/tools/__tests__/assets.test.ts

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { describe, expect, it, vi, beforeEach } from "vitest";
2+
import { handleListAssetsRequest, formatAsset } from "../assets.js";
3+
import {
4+
createErrorResponse,
5+
createSuccessResponse,
6+
formatZodError,
7+
handleGeneralError,
8+
makeDoitRequest,
9+
} from "../../utils/util.js";
10+
11+
// Mock the utility functions
12+
vi.mock("../../utils/util.js", () => ({
13+
createErrorResponse: vi.fn((msg) => ({
14+
content: [{ type: "text", text: msg }],
15+
})),
16+
createSuccessResponse: vi.fn((text) => ({
17+
content: [{ type: "text", text }],
18+
})),
19+
formatZodError: vi.fn((error) => `Formatted Zod Error: ${error.message}`),
20+
handleGeneralError: vi.fn((error, context) => ({
21+
content: [{ type: "text", text: `General Error: ${context}` }],
22+
})),
23+
makeDoitRequest: vi.fn(),
24+
DOIT_API_BASE: "https://api.doit.com",
25+
}));
26+
27+
describe("assets", () => {
28+
describe("formatAsset", () => {
29+
it("should format an asset object correctly", () => {
30+
const mockAsset = {
31+
createTime: 1640995200,
32+
id: "asset-123",
33+
name: "Test Asset",
34+
quantity: 5,
35+
type: "billing_account",
36+
url: "https://console.cloud.google.com/billing/123",
37+
};
38+
39+
const expected = `ID: asset-123
40+
Name: Test Asset
41+
Type: billing_account
42+
Quantity: 5
43+
URL: https://console.cloud.google.com/billing/123
44+
Created: 2022-01-01T00:00:00.000Z
45+
-----------`;
46+
47+
expect(formatAsset(mockAsset)).toBe(expected);
48+
});
49+
});
50+
51+
describe("handleListAssetsRequest", () => {
52+
const mockToken = "fake-token";
53+
54+
beforeEach(() => {
55+
vi.clearAllMocks();
56+
});
57+
58+
it("should call makeDoitRequest with correct parameters and return success response", async () => {
59+
const mockArgs = { pageToken: "next-page" };
60+
const mockApiResponse = {
61+
assets: [
62+
{
63+
createTime: 1640995200,
64+
id: "asset-123",
65+
name: "Test Asset",
66+
quantity: 5,
67+
type: "billing_account",
68+
url: "https://console.cloud.google.com/billing/123",
69+
},
70+
],
71+
pageToken: "another-page",
72+
rowCount: 1,
73+
};
74+
(makeDoitRequest as vi.Mock).mockResolvedValue(mockApiResponse);
75+
76+
const response = await handleListAssetsRequest(mockArgs, mockToken);
77+
78+
expect(makeDoitRequest).toHaveBeenCalledWith(
79+
"https://api.doit.com/billing/v1/assets?pageToken=next-page",
80+
mockToken,
81+
{ method: "GET", customerContext: undefined }
82+
);
83+
expect(createSuccessResponse).toHaveBeenCalledWith(
84+
expect.stringContaining("Found 1 assets:")
85+
);
86+
expect(response).toEqual({
87+
content: [
88+
{ type: "text", text: expect.stringContaining("Found 1 assets:") },
89+
],
90+
});
91+
});
92+
93+
it("should handle no assets found", async () => {
94+
const mockArgs = {};
95+
const mockApiResponse = {
96+
assets: [],
97+
pageToken: "",
98+
rowCount: 0,
99+
};
100+
(makeDoitRequest as vi.Mock).mockResolvedValue(mockApiResponse);
101+
102+
const response = await handleListAssetsRequest(mockArgs, mockToken);
103+
104+
expect(makeDoitRequest).toHaveBeenCalledWith(
105+
"https://api.doit.com/billing/v1/assets",
106+
mockToken,
107+
{ method: "GET", customerContext: undefined }
108+
);
109+
expect(createSuccessResponse).toHaveBeenCalledWith(
110+
"No assets found for this customer context."
111+
);
112+
expect(response).toEqual({
113+
content: [
114+
{
115+
type: "text",
116+
text: "No assets found for this customer context.",
117+
},
118+
],
119+
});
120+
});
121+
122+
it("should handle API request failure", async () => {
123+
const mockArgs = {};
124+
(makeDoitRequest as vi.Mock).mockResolvedValue(null);
125+
126+
const response = await handleListAssetsRequest(mockArgs, mockToken);
127+
128+
expect(makeDoitRequest).toHaveBeenCalledWith(
129+
"https://api.doit.com/billing/v1/assets",
130+
mockToken,
131+
{ method: "GET", customerContext: undefined }
132+
);
133+
expect(createErrorResponse).toHaveBeenCalledWith(
134+
"Failed to retrieve assets data"
135+
);
136+
expect(response).toEqual({
137+
content: [{ type: "text", text: "Failed to retrieve assets data" }],
138+
});
139+
});
140+
141+
it("should handle ZodError for invalid arguments", async () => {
142+
const mockArgs = { pageToken: 123 }; // Invalid pageToken type
143+
const response = await handleListAssetsRequest(mockArgs, mockToken);
144+
145+
expect(formatZodError).toHaveBeenCalled();
146+
expect(createErrorResponse).toHaveBeenCalled();
147+
expect(response).toEqual({
148+
content: [
149+
{
150+
type: "text",
151+
text: expect.stringContaining("Formatted Zod Error:"),
152+
},
153+
],
154+
});
155+
});
156+
157+
it("should handle general errors", async () => {
158+
const mockArgs = {};
159+
(makeDoitRequest as vi.Mock).mockRejectedValue(
160+
new Error("Network error")
161+
);
162+
163+
const response = await handleListAssetsRequest(mockArgs, mockToken);
164+
165+
expect(handleGeneralError).toHaveBeenCalledWith(
166+
expect.any(Error),
167+
"making DoiT API request"
168+
);
169+
expect(response).toEqual({
170+
content: [
171+
{ type: "text", text: "General Error: making DoiT API request" },
172+
],
173+
});
174+
});
175+
176+
it("should include page token in response when provided", async () => {
177+
const mockArgs = {};
178+
const mockApiResponse = {
179+
assets: [
180+
{
181+
createTime: 1640995200,
182+
id: "asset-123",
183+
name: "Test Asset",
184+
quantity: 5,
185+
type: "billing_account",
186+
url: "https://console.cloud.google.com/billing/123",
187+
},
188+
],
189+
pageToken: "next-page-token",
190+
rowCount: 1,
191+
};
192+
(makeDoitRequest as vi.Mock).mockResolvedValue(mockApiResponse);
193+
194+
const response = await handleListAssetsRequest(mockArgs, mockToken);
195+
196+
expect(createSuccessResponse).toHaveBeenCalledWith(
197+
expect.stringContaining("Page token: next-page-token")
198+
);
199+
});
200+
});
201+
});

src/tools/__tests__/dimensions.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Type: fixed
6767
const response = await handleDimensionsRequest(mockArgs, mockToken);
6868

6969
expect(makeDoitRequest).toHaveBeenCalledWith(
70-
"https://api.doit.com/analytics/v1/dimensions?filter=type%3Afixed&pageToken=next-page&maxResults=300",
70+
"https://api.doit.com/analytics/v1/dimensions?filter=type%3Afixed&pageToken=next-page&maxResults=200",
7171
mockToken,
7272
{ method: "GET", customerContext: undefined }
7373
);
@@ -93,7 +93,7 @@ Type: fixed
9393
const response = await handleDimensionsRequest(mockArgs, mockToken);
9494

9595
expect(makeDoitRequest).toHaveBeenCalledWith(
96-
"https://api.doit.com/analytics/v1/dimensions?filter=type%3Ainvalid&maxResults=300",
96+
"https://api.doit.com/analytics/v1/dimensions?filter=type%3Ainvalid&maxResults=200",
9797
mockToken,
9898
{ method: "GET", customerContext: undefined }
9999
);
@@ -117,7 +117,7 @@ Type: fixed
117117
const response = await handleDimensionsRequest(mockArgs, mockToken);
118118

119119
expect(makeDoitRequest).toHaveBeenCalledWith(
120-
"https://api.doit.com/analytics/v1/dimensions?maxResults=300",
120+
"https://api.doit.com/analytics/v1/dimensions?maxResults=200",
121121
mockToken,
122122
{ method: "GET", customerContext: undefined }
123123
);

0 commit comments

Comments
 (0)