@langwatch/mcp-server 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/LICENSE +7 -0
- package/dist/archive-project-HZBRPR2S.js +19 -0
- package/dist/{archive-scenario-YKGLJ4MZ.js → archive-scenario-P5RRITRM.js} +3 -3
- package/dist/{archive-suite-QFNDO3Y3.js → archive-suite-NUUFD2EM.js} +3 -3
- package/dist/{assign-prompt-tag-Y2B4CBBT.js → assign-prompt-tag-FKCCZPML.js} +2 -2
- package/dist/{chunk-YH2IBTEL.js → chunk-42ZHHHIJ.js} +14 -3
- package/dist/chunk-ADSJ2FDA.js +44 -0
- package/dist/{chunk-L6QENXYA.js → chunk-BB7EOKHR.js} +2 -2
- package/dist/chunk-BBJ5WAJB.js +24 -0
- package/dist/{chunk-PZUWCWOS.js → chunk-BYWYM2RP.js} +2 -2
- package/dist/{chunk-NACSR6B7.js → chunk-CCM4ZFGF.js} +2 -2
- package/dist/{chunk-FBRKHHJV.js → chunk-CHKCEZBP.js} +2 -2
- package/dist/{chunk-B57MZ4WI.js → chunk-FVDAZ4XG.js} +763 -90
- package/dist/{chunk-PO7FM7IX.js → chunk-HKY3GW45.js} +2 -2
- package/dist/{chunk-KZNBCKWG.js → chunk-JHOSVOHT.js} +2 -2
- package/dist/{chunk-HDJDFKZR.js → chunk-R7C7OUVG.js} +2 -2
- package/dist/{chunk-OVQF3D5R.js → chunk-TROIWSEQ.js} +2 -2
- package/dist/{chunk-CPUPGDNA.js → chunk-X2SEURT2.js} +2 -2
- package/dist/{chunk-YI3OG4PI.js → chunk-XC62KJIT.js} +3 -3
- package/dist/{create-agent-YRSDH7P7.js → create-agent-K5Q7BFGZ.js} +3 -3
- package/dist/{create-annotation-FYQNKMVJ.js → create-annotation-KN3GE3DK.js} +3 -3
- package/dist/create-api-key-OQI5PUNQ.js +26 -0
- package/dist/{create-dashboard-CORBR4YQ.js → create-dashboard-G4QA5AXP.js} +3 -3
- package/dist/{create-dataset-ZC63T6BB.js → create-dataset-WQSC37N7.js} +3 -3
- package/dist/{create-dataset-records-ILVNVKVR.js → create-dataset-records-B4RL3FLC.js} +3 -3
- package/dist/{create-evaluator-2AU3PXFB.js → create-evaluator-YFFQIR7E.js} +3 -3
- package/dist/create-mcp-server.d.ts +3 -1
- package/dist/create-mcp-server.js +2 -1
- package/dist/create-project-CUVQ67VN.js +44 -0
- package/dist/{create-prompt-NKRJ47YY.js → create-prompt-NGXKSP4X.js} +2 -2
- package/dist/{create-prompt-tag-S4JX3ULL.js → create-prompt-tag-IUKYPVHH.js} +2 -2
- package/dist/{create-scenario-BPRF75KF.js → create-scenario-FNBXALH3.js} +3 -3
- package/dist/{create-suite-4YN7D6K2.js → create-suite-DJEPD4ZB.js} +3 -3
- package/dist/{delete-agent-Y72R2YQA.js → delete-agent-TJI5XBBM.js} +3 -3
- package/dist/{delete-annotation-UHWDMHSH.js → delete-annotation-KRP53TUU.js} +3 -3
- package/dist/{delete-dashboard-LPWT36KN.js → delete-dashboard-N5SZJIR3.js} +3 -3
- package/dist/{delete-dataset-GGPQRHKX.js → delete-dataset-KDUOHL2P.js} +3 -3
- package/dist/{delete-dataset-records-LTG5LT26.js → delete-dataset-records-DM4W3PAW.js} +3 -3
- package/dist/{delete-evaluator-F7QLXDHU.js → delete-evaluator-7ZATLJOA.js} +3 -3
- package/dist/{delete-prompt-tag-B5UKZL45.js → delete-prompt-tag-I6HNMSVJ.js} +2 -2
- package/dist/{delete-workflow-4DGUDIDE.js → delete-workflow-QEO74JQR.js} +3 -3
- package/dist/{get-agent-YEVICUX4.js → get-agent-3KDWCP5N.js} +3 -3
- package/dist/{get-analytics-3W5CMU3F.js → get-analytics-GPVJX7SF.js} +2 -2
- package/dist/{get-dashboard-NCQHN6MW.js → get-dashboard-YPTIE5YY.js} +3 -3
- package/dist/{get-dataset-KBK6PH36.js → get-dataset-6DQFYITM.js} +3 -3
- package/dist/{get-evaluator-QDUL5LXR.js → get-evaluator-PCM6XQAU.js} +3 -3
- package/dist/get-project-JCMWXVLU.js +26 -0
- package/dist/{get-prompt-XCSSNRKR.js → get-prompt-5XAGWWBG.js} +2 -2
- package/dist/{get-scenario-FNRKOY7E.js → get-scenario-7XBCDZWX.js} +3 -3
- package/dist/{get-simulation-run-WOEKUF7K.js → get-simulation-run-AV24FJWI.js} +3 -3
- package/dist/{get-suite-U5AOD5TC.js → get-suite-4DBVHFEM.js} +3 -3
- package/dist/{get-trace-MUWEJM2Z.js → get-trace-MRBDS3HJ.js} +2 -2
- package/dist/{get-workflow-SOA7MT7L.js → get-workflow-UR4Y6UFX.js} +3 -3
- package/dist/{http-server-7DMOCB75.js → http-server-XUQLIFE7.js} +5 -3
- package/dist/index.js +1175 -222
- package/dist/{langwatch-api-annotations-Q57NDUVR.js → langwatch-api-annotations-ZJDRB4JH.js} +3 -3
- package/dist/{langwatch-api-dashboards-VBWWXV3Y.js → langwatch-api-dashboards-WARLI6NF.js} +3 -3
- package/dist/{langwatch-api-monitors-L3IY7FDT.js → langwatch-api-monitors-4XKXHYCG.js} +2 -2
- package/dist/{langwatch-api-secrets-S5HHF3XR.js → langwatch-api-secrets-6SHK5G7Q.js} +2 -2
- package/dist/{langwatch-api-triggers-ME33QE26.js → langwatch-api-triggers-5SX6R2WU.js} +2 -2
- package/dist/{langwatch-api-workflows-4CJQAYP2.js → langwatch-api-workflows-NLJENNXE.js} +3 -3
- package/dist/{list-agents-3HGIBJFM.js → list-agents-NECO7MPI.js} +3 -3
- package/dist/{list-annotations-KWXYVEPI.js → list-annotations-3OJEO774.js} +3 -3
- package/dist/list-api-keys-AWW7ROLB.js +37 -0
- package/dist/{list-dashboards-VQ2LH666.js → list-dashboards-DVPVVVKW.js} +3 -3
- package/dist/{list-dataset-records-RNRGROJP.js → list-dataset-records-I4BYORF5.js} +3 -3
- package/dist/{list-datasets-I4A2LT2S.js → list-datasets-WUAE5HGX.js} +3 -3
- package/dist/{list-evaluators-AKKRJ5MX.js → list-evaluators-B6OSVAM2.js} +3 -3
- package/dist/{list-model-providers-DVFEHFHZ.js → list-model-providers-NL6IP4UZ.js} +3 -3
- package/dist/list-projects-5FHMJ4PQ.js +42 -0
- package/dist/{list-prompt-tags-PL5OISUA.js → list-prompt-tags-Z2DD6FLQ.js} +2 -2
- package/dist/{list-prompts-7VTFWE2Z.js → list-prompts-VNXKHK7T.js} +2 -2
- package/dist/{list-scenarios-5BZWZ6ZN.js → list-scenarios-CFZHXL76.js} +3 -3
- package/dist/{list-simulation-runs-WCZ6RLPE.js → list-simulation-runs-AM2BXLBW.js} +3 -3
- package/dist/{list-suites-RRQ2JX5Y.js → list-suites-ZDOHAAXF.js} +3 -3
- package/dist/{list-workflows-UQB5FCX5.js → list-workflows-WBRBKLIT.js} +3 -3
- package/dist/{rename-prompt-tag-TURD6KYU.js → rename-prompt-tag-A4RJUK7Z.js} +2 -2
- package/dist/revoke-api-key-ZSPSPF5G.js +15 -0
- package/dist/{run-agent-BEJ4K4CM.js → run-agent-JFNDY7MR.js} +3 -3
- package/dist/{run-suite-U7V432G2.js → run-suite-TH7LO5TJ.js} +3 -3
- package/dist/{run-workflow-5WX7DK5T.js → run-workflow-CK3JP5U2.js} +3 -3
- package/dist/{search-traces-ZCHNOMTX.js → search-traces-3BBVYPC3.js} +2 -2
- package/dist/{set-model-provider-6CFHMCNE.js → set-model-provider-WOC5FNMV.js} +3 -3
- package/dist/{update-agent-UE6UWLZP.js → update-agent-YMEV5MTL.js} +3 -3
- package/dist/{update-dataset-4XQZZE4S.js → update-dataset-EYT5POFM.js} +3 -3
- package/dist/{update-dataset-record-GNNM5TKM.js → update-dataset-record-VFF5JTX4.js} +3 -3
- package/dist/{update-evaluator-LHVDXIZD.js → update-evaluator-6IJHFG2E.js} +3 -3
- package/dist/update-project-OIT4WPFI.js +24 -0
- package/dist/{update-prompt-3XEHECJ7.js → update-prompt-ZU2DD6KM.js} +2 -2
- package/dist/{update-scenario-DFPREYCD.js → update-scenario-3VUTPDRU.js} +3 -3
- package/dist/{update-suite-RATU5THO.js → update-suite-C6JFDQHL.js} +3 -3
- package/package.json +2 -2
- package/pnpm-workspace.yaml +7 -0
- package/pyproject.toml +3 -0
- package/src/__tests__/experiment-list-tools.unit.test.ts +157 -0
- package/src/__tests__/experiment-status.unit.test.ts +194 -0
- package/src/__tests__/get-experiment-results.unit.test.ts +313 -0
- package/src/__tests__/project-api-key-tools.unit.test.ts +451 -0
- package/src/create-mcp-server.d.ts +3 -1
- package/src/create-mcp-server.ts +263 -13
- package/src/http-server.ts +5 -0
- package/src/langwatch-api-api-keys.ts +54 -0
- package/src/langwatch-api-projects.ts +82 -0
- package/src/langwatch-api.ts +17 -3
- package/src/tools/archive-project.ts +14 -0
- package/src/tools/create-api-key.ts +29 -0
- package/src/tools/create-project.ts +45 -0
- package/src/tools/experiment-run-status.ts +36 -0
- package/src/tools/get-experiment-results.ts +306 -0
- package/src/tools/get-project.ts +20 -0
- package/src/tools/list-api-keys.ts +33 -0
- package/src/tools/list-experiment-runs.ts +142 -0
- package/src/tools/list-experiments.ts +95 -0
- package/src/tools/list-projects.ts +41 -0
- package/src/tools/revoke-api-key.ts +9 -0
- package/src/tools/run-experiment.ts +182 -0
- package/src/tools/update-project.ts +22 -0
- package/uv.lock +7 -3
- package/dist/run-evaluation-AJ7WZNQD.js +0 -64
- package/src/tools/run-evaluation.ts +0 -87
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("../langwatch-api-projects.js", () => ({
|
|
4
|
+
listProjects: vi.fn(),
|
|
5
|
+
getProject: vi.fn(),
|
|
6
|
+
createProject: vi.fn(),
|
|
7
|
+
updateProject: vi.fn(),
|
|
8
|
+
archiveProject: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("../langwatch-api-api-keys.js", () => ({
|
|
12
|
+
listApiKeys: vi.fn(),
|
|
13
|
+
createApiKey: vi.fn(),
|
|
14
|
+
revokeApiKey: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
listProjects,
|
|
19
|
+
getProject,
|
|
20
|
+
createProject,
|
|
21
|
+
updateProject,
|
|
22
|
+
archiveProject,
|
|
23
|
+
} from "../langwatch-api-projects.js";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
listApiKeys,
|
|
27
|
+
createApiKey,
|
|
28
|
+
revokeApiKey,
|
|
29
|
+
} from "../langwatch-api-api-keys.js";
|
|
30
|
+
|
|
31
|
+
import { handleListProjects } from "../tools/list-projects.js";
|
|
32
|
+
import { handleGetProject } from "../tools/get-project.js";
|
|
33
|
+
import { handleCreateProject } from "../tools/create-project.js";
|
|
34
|
+
import { handleUpdateProject } from "../tools/update-project.js";
|
|
35
|
+
import { handleArchiveProject } from "../tools/archive-project.js";
|
|
36
|
+
import { handleListApiKeys } from "../tools/list-api-keys.js";
|
|
37
|
+
import { handleCreateApiKey } from "../tools/create-api-key.js";
|
|
38
|
+
import { handleRevokeApiKey } from "../tools/revoke-api-key.js";
|
|
39
|
+
|
|
40
|
+
const mockListProjects = vi.mocked(listProjects);
|
|
41
|
+
const mockGetProject = vi.mocked(getProject);
|
|
42
|
+
const mockCreateProject = vi.mocked(createProject);
|
|
43
|
+
const mockUpdateProject = vi.mocked(updateProject);
|
|
44
|
+
const mockArchiveProject = vi.mocked(archiveProject);
|
|
45
|
+
const mockListApiKeys = vi.mocked(listApiKeys);
|
|
46
|
+
const mockCreateApiKey = vi.mocked(createApiKey);
|
|
47
|
+
const mockRevokeApiKey = vi.mocked(revokeApiKey);
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// --- Project Tools ---
|
|
54
|
+
|
|
55
|
+
describe("handleListProjects()", () => {
|
|
56
|
+
const sampleProjects = [
|
|
57
|
+
{
|
|
58
|
+
id: "proj_abc123",
|
|
59
|
+
name: "My Project",
|
|
60
|
+
slug: "my-project",
|
|
61
|
+
language: "python",
|
|
62
|
+
framework: "openai",
|
|
63
|
+
teamId: "team_1",
|
|
64
|
+
piiRedactionLevel: "ESSENTIAL",
|
|
65
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
66
|
+
updatedAt: "2024-06-01T00:00:00Z",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "proj_def456",
|
|
70
|
+
name: "Another Project",
|
|
71
|
+
slug: "another-project",
|
|
72
|
+
language: "typescript",
|
|
73
|
+
framework: "langchain",
|
|
74
|
+
teamId: "team_1",
|
|
75
|
+
piiRedactionLevel: "DISABLED",
|
|
76
|
+
createdAt: "2024-02-01T00:00:00Z",
|
|
77
|
+
updatedAt: "2024-07-01T00:00:00Z",
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
describe("when projects exist", () => {
|
|
82
|
+
let result: string;
|
|
83
|
+
|
|
84
|
+
beforeEach(async () => {
|
|
85
|
+
mockListProjects.mockResolvedValue({
|
|
86
|
+
data: sampleProjects,
|
|
87
|
+
pagination: { page: 1, limit: 100, total: 2, totalPages: 1 },
|
|
88
|
+
});
|
|
89
|
+
result = await handleListProjects();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("includes project count", () => {
|
|
93
|
+
expect(result).toContain("2 total");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("includes project names", () => {
|
|
97
|
+
expect(result).toContain("My Project");
|
|
98
|
+
expect(result).toContain("Another Project");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("includes project IDs", () => {
|
|
102
|
+
expect(result).toContain("proj_abc123");
|
|
103
|
+
expect(result).toContain("proj_def456");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("includes language and framework", () => {
|
|
107
|
+
expect(result).toContain("python");
|
|
108
|
+
expect(result).toContain("openai");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("when no projects exist", () => {
|
|
113
|
+
it("returns empty state with tip", async () => {
|
|
114
|
+
mockListProjects.mockResolvedValue({
|
|
115
|
+
data: [],
|
|
116
|
+
pagination: { page: 1, limit: 100, total: 0, totalPages: 0 },
|
|
117
|
+
});
|
|
118
|
+
const result = await handleListProjects();
|
|
119
|
+
expect(result).toContain("No projects found");
|
|
120
|
+
expect(result).toContain("platform_create_project");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("when paginated", () => {
|
|
125
|
+
it("shows pagination hint", async () => {
|
|
126
|
+
mockListProjects.mockResolvedValue({
|
|
127
|
+
data: [sampleProjects[0]!],
|
|
128
|
+
pagination: { page: 1, limit: 1, total: 5, totalPages: 5 },
|
|
129
|
+
});
|
|
130
|
+
const result = await handleListProjects({ limit: 1 });
|
|
131
|
+
expect(result).toContain("page 1 of 5");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("handleGetProject()", () => {
|
|
137
|
+
it("returns formatted project details", async () => {
|
|
138
|
+
mockGetProject.mockResolvedValue({
|
|
139
|
+
id: "proj_abc123",
|
|
140
|
+
name: "My Project",
|
|
141
|
+
slug: "my-project",
|
|
142
|
+
language: "python",
|
|
143
|
+
framework: "openai",
|
|
144
|
+
teamId: "team_1",
|
|
145
|
+
piiRedactionLevel: "ESSENTIAL",
|
|
146
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
147
|
+
updatedAt: "2024-06-01T00:00:00Z",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result = await handleGetProject({ id: "proj_abc123" });
|
|
151
|
+
|
|
152
|
+
expect(result).toContain("My Project");
|
|
153
|
+
expect(result).toContain("proj_abc123");
|
|
154
|
+
expect(result).toContain("python");
|
|
155
|
+
expect(result).toContain("ESSENTIAL");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("handleCreateProject()", () => {
|
|
160
|
+
it("returns project details with service API key", async () => {
|
|
161
|
+
mockCreateProject.mockResolvedValue({
|
|
162
|
+
id: "proj_new",
|
|
163
|
+
name: "New Project",
|
|
164
|
+
slug: "new-project",
|
|
165
|
+
language: "typescript",
|
|
166
|
+
framework: "custom",
|
|
167
|
+
teamId: "team_1",
|
|
168
|
+
piiRedactionLevel: "DISABLED",
|
|
169
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
170
|
+
updatedAt: "2024-01-01T00:00:00Z",
|
|
171
|
+
serviceApiKey: "lw_sk_abc123secret",
|
|
172
|
+
serviceApiKeyId: "key_svc_1",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const result = await handleCreateProject({
|
|
176
|
+
name: "New Project",
|
|
177
|
+
language: "typescript",
|
|
178
|
+
framework: "custom",
|
|
179
|
+
newTeamName: "My Team",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(result).toContain("created successfully");
|
|
183
|
+
expect(result).toContain("New Project");
|
|
184
|
+
expect(result).toContain("proj_new");
|
|
185
|
+
expect(result).toContain("lw_sk_abc123secret");
|
|
186
|
+
expect(result).toContain("Save the service API key now");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("handleUpdateProject()", () => {
|
|
191
|
+
it("returns updated project details", async () => {
|
|
192
|
+
mockUpdateProject.mockResolvedValue({
|
|
193
|
+
id: "proj_abc123",
|
|
194
|
+
name: "Renamed Project",
|
|
195
|
+
slug: "my-project",
|
|
196
|
+
language: "python",
|
|
197
|
+
framework: "openai",
|
|
198
|
+
teamId: "team_1",
|
|
199
|
+
piiRedactionLevel: "STRICT",
|
|
200
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
201
|
+
updatedAt: "2024-08-01T00:00:00Z",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const result = await handleUpdateProject({
|
|
205
|
+
id: "proj_abc123",
|
|
206
|
+
name: "Renamed Project",
|
|
207
|
+
piiRedactionLevel: "STRICT",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(result).toContain("updated successfully");
|
|
211
|
+
expect(result).toContain("Renamed Project");
|
|
212
|
+
expect(result).toContain("STRICT");
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("handleArchiveProject()", () => {
|
|
217
|
+
it("returns archive confirmation", async () => {
|
|
218
|
+
mockArchiveProject.mockResolvedValue({
|
|
219
|
+
id: "proj_abc123",
|
|
220
|
+
name: "My Project",
|
|
221
|
+
archivedAt: "2024-09-01T00:00:00Z",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = await handleArchiveProject({ id: "proj_abc123" });
|
|
225
|
+
|
|
226
|
+
expect(result).toContain("archived successfully");
|
|
227
|
+
expect(result).toContain("proj_abc123");
|
|
228
|
+
expect(result).toContain("My Project");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// --- API Key Tools ---
|
|
233
|
+
|
|
234
|
+
describe("handleListApiKeys()", () => {
|
|
235
|
+
describe("when keys exist", () => {
|
|
236
|
+
it("shows key metadata and status", async () => {
|
|
237
|
+
mockListApiKeys.mockResolvedValue({
|
|
238
|
+
data: [
|
|
239
|
+
{
|
|
240
|
+
id: "key_1",
|
|
241
|
+
name: "Production Key",
|
|
242
|
+
description: "Main production API key",
|
|
243
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
244
|
+
expiresAt: null,
|
|
245
|
+
lastUsedAt: "2024-06-01T00:00:00Z",
|
|
246
|
+
revokedAt: null,
|
|
247
|
+
roleBindings: [
|
|
248
|
+
{ id: "rb_1", role: "ADMIN", scopeType: "PROJECT", scopeId: "proj_abc" },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "key_2",
|
|
253
|
+
name: "Revoked Key",
|
|
254
|
+
description: null,
|
|
255
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
256
|
+
expiresAt: null,
|
|
257
|
+
lastUsedAt: null,
|
|
258
|
+
revokedAt: "2024-05-01T00:00:00Z",
|
|
259
|
+
roleBindings: [],
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const result = await handleListApiKeys();
|
|
265
|
+
|
|
266
|
+
expect(result).toContain("2 total");
|
|
267
|
+
expect(result).toContain("Production Key");
|
|
268
|
+
expect(result).toContain("ACTIVE");
|
|
269
|
+
expect(result).toContain("Revoked Key");
|
|
270
|
+
expect(result).toContain("REVOKED");
|
|
271
|
+
expect(result).toContain("ADMIN on PROJECT:proj_abc");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("when no keys exist", () => {
|
|
276
|
+
it("returns empty state with tip", async () => {
|
|
277
|
+
mockListApiKeys.mockResolvedValue({ data: [] });
|
|
278
|
+
|
|
279
|
+
const result = await handleListApiKeys();
|
|
280
|
+
|
|
281
|
+
expect(result).toContain("No API keys found");
|
|
282
|
+
expect(result).toContain("platform_create_api_key");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("handleCreateApiKey()", () => {
|
|
288
|
+
it("returns token with save warning", async () => {
|
|
289
|
+
mockCreateApiKey.mockResolvedValue({
|
|
290
|
+
token: "lw_pat_secret_token_123",
|
|
291
|
+
apiKey: {
|
|
292
|
+
id: "key_new",
|
|
293
|
+
name: "My New Key",
|
|
294
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const result = await handleCreateApiKey({
|
|
299
|
+
keyType: "personal",
|
|
300
|
+
name: "My New Key",
|
|
301
|
+
bindings: [
|
|
302
|
+
{ role: "ADMIN", scopeType: "ORGANIZATION", scopeId: "org_1" },
|
|
303
|
+
],
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
expect(result).toContain("created successfully");
|
|
307
|
+
expect(result).toContain("My New Key");
|
|
308
|
+
expect(result).toContain("lw_pat_secret_token_123");
|
|
309
|
+
expect(result).toContain("Save this token now");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("handleRevokeApiKey()", () => {
|
|
314
|
+
it("confirms revocation", async () => {
|
|
315
|
+
mockRevokeApiKey.mockResolvedValue({ success: true });
|
|
316
|
+
|
|
317
|
+
const result = await handleRevokeApiKey({ id: "key_123" });
|
|
318
|
+
|
|
319
|
+
expect(result).toContain("key_123");
|
|
320
|
+
expect(result).toContain("revoked successfully");
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// --- Error-path tests ---
|
|
325
|
+
|
|
326
|
+
describe("handleCreateProject() validation", () => {
|
|
327
|
+
describe("when neither teamId nor newTeamName is provided", () => {
|
|
328
|
+
it("returns a validation error without calling the API", async () => {
|
|
329
|
+
const result = await handleCreateProject({
|
|
330
|
+
name: "Test Project",
|
|
331
|
+
language: "python",
|
|
332
|
+
framework: "openai",
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
expect(result).toContain("Error");
|
|
336
|
+
expect(result).toContain("teamId");
|
|
337
|
+
expect(result).toContain("newTeamName");
|
|
338
|
+
expect(mockCreateProject).not.toHaveBeenCalled();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("when API does not return a serviceApiKey", () => {
|
|
343
|
+
it("throws an error to prevent silent data loss", async () => {
|
|
344
|
+
mockCreateProject.mockResolvedValue({
|
|
345
|
+
id: "proj_new",
|
|
346
|
+
name: "New Project",
|
|
347
|
+
slug: "new-project",
|
|
348
|
+
language: "typescript",
|
|
349
|
+
framework: "custom",
|
|
350
|
+
teamId: "team_1",
|
|
351
|
+
piiRedactionLevel: "DISABLED",
|
|
352
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
353
|
+
updatedAt: "2024-01-01T00:00:00Z",
|
|
354
|
+
serviceApiKey: "",
|
|
355
|
+
serviceApiKeyId: "key_svc_1",
|
|
356
|
+
} as never);
|
|
357
|
+
|
|
358
|
+
await expect(
|
|
359
|
+
handleCreateProject({
|
|
360
|
+
name: "New Project",
|
|
361
|
+
language: "typescript",
|
|
362
|
+
framework: "custom",
|
|
363
|
+
newTeamName: "My Team",
|
|
364
|
+
}),
|
|
365
|
+
).rejects.toThrow("service API key");
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("handleListProjects() edge cases", () => {
|
|
371
|
+
describe("when API returns non-array data", () => {
|
|
372
|
+
it("returns empty state message", async () => {
|
|
373
|
+
mockListProjects.mockResolvedValue({
|
|
374
|
+
data: null as never,
|
|
375
|
+
pagination: { page: 1, limit: 100, total: 0, totalPages: 0 },
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const result = await handleListProjects();
|
|
379
|
+
|
|
380
|
+
expect(result).toContain("No projects found");
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe("when API call throws", () => {
|
|
385
|
+
it("propagates the error", async () => {
|
|
386
|
+
mockListProjects.mockRejectedValue(
|
|
387
|
+
new Error('LangWatch API error 403: {"error":"Forbidden","message":"Insufficient permissions"}'),
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
await expect(handleListProjects()).rejects.toThrow("403");
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe("handleListApiKeys() edge cases", () => {
|
|
396
|
+
describe("when a key has a past expiration date", () => {
|
|
397
|
+
it("shows EXPIRED status", async () => {
|
|
398
|
+
mockListApiKeys.mockResolvedValue({
|
|
399
|
+
data: [
|
|
400
|
+
{
|
|
401
|
+
id: "key_exp",
|
|
402
|
+
name: "Expired Key",
|
|
403
|
+
description: null,
|
|
404
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
405
|
+
expiresAt: "2024-06-01T00:00:00Z",
|
|
406
|
+
lastUsedAt: null,
|
|
407
|
+
revokedAt: null,
|
|
408
|
+
roleBindings: [],
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const result = await handleListApiKeys();
|
|
414
|
+
|
|
415
|
+
expect(result).toContain("EXPIRED");
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe("when a key has a future expiration date", () => {
|
|
420
|
+
it("shows ACTIVE status", async () => {
|
|
421
|
+
mockListApiKeys.mockResolvedValue({
|
|
422
|
+
data: [
|
|
423
|
+
{
|
|
424
|
+
id: "key_future",
|
|
425
|
+
name: "Future Key",
|
|
426
|
+
description: null,
|
|
427
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
428
|
+
expiresAt: "2099-12-31T23:59:59Z",
|
|
429
|
+
lastUsedAt: null,
|
|
430
|
+
revokedAt: null,
|
|
431
|
+
roleBindings: [],
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const result = await handleListApiKeys();
|
|
437
|
+
|
|
438
|
+
expect(result).toContain("ACTIVE");
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
describe("when API returns non-array data", () => {
|
|
443
|
+
it("returns empty state message", async () => {
|
|
444
|
+
mockListApiKeys.mockResolvedValue({ data: null as never });
|
|
445
|
+
|
|
446
|
+
const result = await handleListApiKeys();
|
|
447
|
+
|
|
448
|
+
expect(result).toContain("No API keys found");
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Minimal type declaration — avoids pulling mcp-server's full dependency tree
|
|
2
2
|
// into the langwatch app's typecheck scope. The actual return type is McpServer,
|
|
3
|
-
// but we only expose the connect()
|
|
3
|
+
// but we only expose the connect() and tool() methods that handler.ts +
|
|
4
|
+
// in-app tool registrations (e.g. registerGovernanceMcpTools) use.
|
|
4
5
|
export declare function createMcpServer(): {
|
|
5
6
|
connect(transport: unknown): Promise<void>;
|
|
7
|
+
tool(...args: unknown[]): unknown;
|
|
6
8
|
};
|