@timetick/mcp 0.1.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/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # @timetick/mcp
2
+
3
+ Model Context Protocol (MCP) server for [Timetick](https://timetick.io) — drive EV charger simulators, OCPP tests and real hardware from an AI client, in plain language.
4
+
5
+ Built on the official [`@timetick/sdk`](https://www.npmjs.com/package/@timetick/sdk).
6
+
7
+ ## Quick start
8
+
9
+ Generate an API key in the Timetick portal under **Account (top-right corner) → API keys**, then register the server with your MCP client. For Claude Desktop, in `claude_desktop_config.json`:
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "timetick": {
15
+ "command": "npx",
16
+ "args": ["-y", "@timetick/mcp"],
17
+ "env": { "TIMETICK_API_TOKEN": "your_api_key_here" }
18
+ }
19
+ }
20
+ }
21
+ ```
22
+
23
+ Restart the client and ask for something:
24
+
25
+ - _"list my simulators"_
26
+ - _"power on CP-001"_
27
+ - _"create a full charge cycle test and run it on CP-001"_
28
+
29
+ Cursor, Claude Code and other MCP clients take the same command and environment variable.
30
+
31
+ ## Configuration
32
+
33
+ | Env var | Required | Description |
34
+ | -------------------- | -------- | ------------------------------------------------------------ |
35
+ | `TIMETICK_API_TOKEN` | yes | Your Timetick API key, sent as `x-api-key` |
36
+ | `TIMETICK_API_URL` | no | API endpoint (defaults to `https://app.timetick.io/graphql`) |
37
+
38
+ ## Projects and sim worlds
39
+
40
+ Most tools work inside a project's sim world and resolve it for you.
41
+
42
+ | Situation | What happens |
43
+ | ------------------------ | ---------------------------------------------------------------- |
44
+ | Your key has one project | It is selected automatically |
45
+ | Your key has several | Call `use_project` once, by name or id; it holds for the session |
46
+ | Any time | `get_context` reports the active project and sim world |
47
+
48
+ Pass `project` to a tool to target a different project for that call alone. The session's active project stays where it is.
49
+
50
+ ## Tools
51
+
52
+ Projects, simulators and tests are addressed by name or by id, whichever you have.
53
+
54
+ ### Context
55
+
56
+ | Tool | Description |
57
+ | ------------- | -------------------------------- |
58
+ | `get_context` | The active project and sim world |
59
+ | `use_project` | Switch the active project |
60
+
61
+ ### Inspect
62
+
63
+ | Tool | Description |
64
+ | ----------------- | ------------------------------------------------------- |
65
+ | `list_projects` | Projects your API key can reach |
66
+ | `list_simulators` | Virtual chargers, filtered by name or protocol |
67
+ | `get_simulator` | One simulator: network status, power status, connectors |
68
+ | `list_hardware` | Physical OCPP devices |
69
+ | `list_actions` | The step library available for building tests |
70
+
71
+ Lists come back 20 rows per page. Pass `page` and `pageSize` for the rest; every reply reports the total.
72
+
73
+ ### Control simulators
74
+
75
+ | Tool | Description |
76
+ | ---------------------- | -------------------------------------------------------------------- |
77
+ | `power_on_simulator` | Boot a simulator and connect it to the OCPP server |
78
+ | `power_off_simulator` | Shut a simulator down |
79
+ | `run_simulator_action` | Run one step — reset, plug in, plug out — with `key=value` arguments |
80
+
81
+ ### Tests
82
+
83
+ | Tool | Description |
84
+ | ------------- | ----------------------------------------------- |
85
+ | `list_tests` | Tests in the active sim world |
86
+ | `get_test` | One test, with its device requirements |
87
+ | `create_test` | Build a test from an ordered list of step names |
88
+
89
+ ### Run and monitor
90
+
91
+ | Tool | Description |
92
+ | ---------------- | --------------------------- |
93
+ | `run_test` | Start a test on a simulator |
94
+ | `get_test_run` | Status of a run |
95
+ | `list_test_runs` | Recent runs |
96
+ | `stop_test_run` | Shut a run down |
97
+
98
+ A simulator runs one test at a time. If it is busy, `run_test` says so and starts nothing — stop the run that holds it with `stop_test_run`, then ask again.
package/dist/index.js ADDED
@@ -0,0 +1,611 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/tools/context.ts
8
+ import { z } from "zod";
9
+
10
+ // src/sdk.ts
11
+ import { createClient } from "@timetick/sdk";
12
+ var client = null;
13
+ function sdk() {
14
+ if (client) return client;
15
+ const apiKey = process.env.TIMETICK_API_TOKEN;
16
+ if (!apiKey) {
17
+ throw new Error(
18
+ "TIMETICK_API_TOKEN is not set. Generate an API key in the Timetick portal (Account \u2192 API keys) and expose it to the MCP server as TIMETICK_API_TOKEN."
19
+ );
20
+ }
21
+ client = createClient({ apiKey, endpoint: process.env.TIMETICK_API_URL });
22
+ return client;
23
+ }
24
+
25
+ // src/context.ts
26
+ var ContextError = class extends Error {
27
+ };
28
+ var cached = null;
29
+ async function buildContext(projectId, projectName, source) {
30
+ const worlds = await sdk().simWorlds.list(projectId);
31
+ const world = worlds?.[0];
32
+ if (!world?.id) {
33
+ throw new ContextError(`Project "${projectName}" has no sim world to operate on.`);
34
+ }
35
+ return {
36
+ projectId,
37
+ projectName,
38
+ simWorldId: world.id,
39
+ deviceScopeId: world.deviceScopeId ?? world.id,
40
+ source
41
+ };
42
+ }
43
+ var PROJECT_LIST_LIMIT = 15;
44
+ function formatProjects(projects) {
45
+ const all = projects ?? [];
46
+ const lines = all.slice(0, PROJECT_LIST_LIMIT).map((p) => ` - ${p?.name} (id: ${p?.id})`);
47
+ if (all.length > PROJECT_LIST_LIMIT) {
48
+ lines.push(` \u2026and ${all.length - PROJECT_LIST_LIMIT} more \u2014 call list_projects to see them all.`);
49
+ }
50
+ return lines.join("\n");
51
+ }
52
+ async function findProject(project) {
53
+ const projects = await sdk().projects.list();
54
+ const needle = project.toLowerCase();
55
+ const match = projects?.find(
56
+ (p) => p?.id === project || p?.name?.toLowerCase() === needle
57
+ );
58
+ if (!match?.id) {
59
+ throw new ContextError(
60
+ `No project matching "${project}". Available:
61
+ ${formatProjects(projects)}`
62
+ );
63
+ }
64
+ return { id: match.id, name: match.name ?? "(unnamed)" };
65
+ }
66
+ async function getContext() {
67
+ if (cached) return cached;
68
+ const projects = await sdk().projects.list();
69
+ if (!projects || projects.length === 0) {
70
+ throw new ContextError("No projects are available for this API key.");
71
+ }
72
+ if (projects.length > 1) {
73
+ throw new ContextError(
74
+ `You have ${projects.length} projects. Call \`use_project\` with a project name or id first:
75
+ ${formatProjects(projects)}`
76
+ );
77
+ }
78
+ const only = projects[0];
79
+ cached = await buildContext(only.id, only.name ?? "(unnamed)", "single-project");
80
+ return cached;
81
+ }
82
+ async function useProject(input) {
83
+ const match = await findProject(input.project);
84
+ cached = await buildContext(match.id, match.name, "explicit");
85
+ return cached;
86
+ }
87
+ async function lookupProject(project) {
88
+ const match = await findProject(project);
89
+ return buildContext(match.id, match.name, "explicit");
90
+ }
91
+ function peekContext() {
92
+ return cached;
93
+ }
94
+
95
+ // src/result.ts
96
+ import { TimetickError } from "@timetick/sdk";
97
+ function ok(text, data) {
98
+ const body = data === void 0 ? text : `${text}
99
+
100
+ ${JSON.stringify(data, null, 2)}`;
101
+ return { content: [{ type: "text", text: body }] };
102
+ }
103
+ function fail(message) {
104
+ return { content: [{ type: "text", text: message }], isError: true };
105
+ }
106
+ async function guard(fn) {
107
+ try {
108
+ return await fn();
109
+ } catch (err) {
110
+ if (err instanceof TimetickError) return fail(`Timetick API error: ${err.message}`);
111
+ return fail(err instanceof Error ? err.message : String(err));
112
+ }
113
+ }
114
+
115
+ // src/tools/context.ts
116
+ function registerContextTools(server) {
117
+ server.registerTool(
118
+ "get_context",
119
+ {
120
+ title: "Get active context",
121
+ description: "Show the project and sim world the MCP server is currently operating on. If no project is selected yet and you have only one project, it is auto-selected.",
122
+ inputSchema: {}
123
+ },
124
+ async () => guard(async () => {
125
+ const ctx = peekContext() ?? await getContext();
126
+ return ok(
127
+ `Active project: ${ctx.projectName} (id: ${ctx.projectId}); sim world: ${ctx.simWorldId} [${ctx.source}]`,
128
+ ctx
129
+ );
130
+ })
131
+ );
132
+ server.registerTool(
133
+ "use_project",
134
+ {
135
+ title: "Select project",
136
+ description: "Switch the active project by name or id. Only needed if you have more than one project. Caches the project's sim world for subsequent tools.",
137
+ inputSchema: {
138
+ project: z.string().describe("Project name or id to make active")
139
+ }
140
+ },
141
+ async ({ project }) => guard(async () => {
142
+ const ctx = await useProject({ project });
143
+ return ok(`Active project is now ${ctx.projectName} (id: ${ctx.projectId}).`, ctx);
144
+ })
145
+ );
146
+ }
147
+
148
+ // src/tools/data.ts
149
+ import { z as z3 } from "zod";
150
+
151
+ // src/resolve.ts
152
+ async function resolveSimWorld(project) {
153
+ if (project) return lookupProject(project);
154
+ return getContext();
155
+ }
156
+ async function resolveSimulator(simWorldId, simulator) {
157
+ const needle = simulator.toLowerCase();
158
+ const page = await sdk().simulators.list({ simWorldId, searchValue: simulator });
159
+ const records = page?.records ?? [];
160
+ const exact = records.find((r) => r?.id === simulator) ?? records.find((r) => r?.baseName?.toLowerCase() === needle) ?? (records.length === 1 ? records[0] : void 0);
161
+ if (exact?.id) {
162
+ return { id: exact.id, baseName: exact.baseName ?? "", protocol: String(exact.protocol) };
163
+ }
164
+ const byId = await sdk().simulators.get(simulator).catch(() => null);
165
+ if (byId?.id) {
166
+ return { id: byId.id, baseName: byId.baseName ?? "", protocol: String(byId.protocol) };
167
+ }
168
+ const candidates = records.map((r) => ` - ${r?.baseName} (id: ${r?.id})`).join("\n");
169
+ throw new Error(
170
+ `No simulator matching "${simulator}".` + (candidates ? `
171
+ Candidates:
172
+ ${candidates}` : " No simulators found in this sim world.")
173
+ );
174
+ }
175
+ async function resolveActionId(simWorldId, protocol, actionName) {
176
+ const page = await sdk().actions.list({
177
+ simWorldId,
178
+ searchValue: actionName,
179
+ filters: { isSystem: true, protocol, deviceType: "CHARGER" },
180
+ pagination: { page: 1, pageSize: 50 }
181
+ });
182
+ const records = page?.records ?? [];
183
+ const needle = actionName.toLowerCase();
184
+ const match = records.find((a) => a?.name?.toLowerCase() === needle) ?? records.find((a) => a?.name?.toLowerCase().includes(needle));
185
+ if (!match?.id) {
186
+ const available = records.map((a) => ` - ${a?.name}`).join("\n");
187
+ throw new Error(
188
+ `Action "${actionName}" not found for ${protocol}.` + (available ? `
189
+ Available system actions matching your search:
190
+ ${available}` : "")
191
+ );
192
+ }
193
+ return match.id;
194
+ }
195
+
196
+ // src/pagination.ts
197
+ import { z as z2 } from "zod";
198
+ var DEFAULT_PAGE_SIZE = 20;
199
+ var paginationArgs = {
200
+ page: z2.number().int().min(1).optional().describe("Page number, starting at 1 (default 1)"),
201
+ pageSize: z2.number().int().min(1).max(200).optional().describe(`Results per page (default ${DEFAULT_PAGE_SIZE})`)
202
+ };
203
+ function pagination(page, pageSize, defaultPageSize = DEFAULT_PAGE_SIZE) {
204
+ return { page: page ?? 1, pageSize: pageSize ?? defaultPageSize };
205
+ }
206
+ function pageSummary(noun, count, pageInfo) {
207
+ const total = pageInfo?.totalCount;
208
+ const head = total != null && total !== count ? `Showing ${count} of ${total} ${noun} (page ${pageInfo?.page ?? 1}).` : `Found ${count} ${noun}.`;
209
+ return pageInfo?.hasNextPage ? `${head} More results available \u2014 request the next page.` : head;
210
+ }
211
+
212
+ // src/tools/data.ts
213
+ var protocolEnum = z3.enum(["OCPP_1_6", "OCPP_2_0_1"]);
214
+ function summarizeArgs(params) {
215
+ if (!params || typeof params !== "object") return void 0;
216
+ const summary = {};
217
+ for (const [name, spec] of Object.entries(params)) {
218
+ if (!spec || typeof spec !== "object") continue;
219
+ const { type, allowNull, defaultValue, visible } = spec;
220
+ if (visible === false) continue;
221
+ const parts = [String(type ?? "unknown"), allowNull === false ? "required" : "optional"];
222
+ if (defaultValue !== void 0) parts.push(`default ${JSON.stringify(defaultValue)}`);
223
+ summary[name] = parts.join(", ");
224
+ }
225
+ return Object.keys(summary).length > 0 ? summary : void 0;
226
+ }
227
+ function registerDataTools(server) {
228
+ server.registerTool(
229
+ "list_projects",
230
+ {
231
+ title: "List projects",
232
+ description: "List all projects available to your API key.",
233
+ inputSchema: {}
234
+ },
235
+ async () => guard(async () => {
236
+ const projects = await sdk().projects.list();
237
+ return ok(`Found ${projects?.length ?? 0} project(s).`, projects);
238
+ })
239
+ );
240
+ server.registerTool(
241
+ "list_simulators",
242
+ {
243
+ title: "List simulators",
244
+ description: "List simulators (virtual chargers) in the active project's sim world. Optionally filter by name search or protocol.",
245
+ inputSchema: {
246
+ search: z3.string().optional().describe("Filter by simulator base name"),
247
+ protocol: protocolEnum.optional(),
248
+ ...paginationArgs,
249
+ project: z3.string().optional().describe("Project name or id (defaults to active project)")
250
+ }
251
+ },
252
+ async ({ search, protocol, page, pageSize, project }) => guard(async () => {
253
+ const ctx = await resolveSimWorld(project);
254
+ const result = await sdk().simulators.list({
255
+ simWorldId: ctx.simWorldId,
256
+ searchValue: search,
257
+ filters: protocol ? { protocol } : void 0,
258
+ pagination: pagination(page, pageSize)
259
+ });
260
+ return ok(
261
+ pageSummary("simulator(s)", result?.records?.length ?? 0, result?.pageInfo),
262
+ result
263
+ );
264
+ })
265
+ );
266
+ server.registerTool(
267
+ "get_simulator",
268
+ {
269
+ title: "Get simulator",
270
+ description: "Get a simulator's details and live status (network status, launched/power status, connectors) by id or base name.",
271
+ inputSchema: {
272
+ simulator: z3.string().describe("Simulator id or base name"),
273
+ project: z3.string().optional().describe("Project name or id (defaults to active project)")
274
+ }
275
+ },
276
+ async ({ simulator, project }) => guard(async () => {
277
+ const ctx = await resolveSimWorld(project);
278
+ const resolved = await resolveSimulator(ctx.simWorldId, simulator);
279
+ const details = await sdk().simulators.get(resolved.id);
280
+ return ok(`Simulator ${resolved.baseName} (id: ${resolved.id}).`, details);
281
+ })
282
+ );
283
+ server.registerTool(
284
+ "list_hardware",
285
+ {
286
+ title: "List hardware",
287
+ description: "List physical OCPP hardware devices in the active project's sim world.",
288
+ inputSchema: {
289
+ search: z3.string().optional().describe("Filter by serial number / model"),
290
+ ...paginationArgs,
291
+ project: z3.string().optional().describe("Project name or id (defaults to active project)")
292
+ }
293
+ },
294
+ async ({ search, page, pageSize, project }) => guard(async () => {
295
+ const ctx = await resolveSimWorld(project);
296
+ const result = await sdk().hardware.list({
297
+ deviceScopeId: ctx.deviceScopeId,
298
+ searchValue: search,
299
+ pagination: pagination(page, pageSize)
300
+ });
301
+ return ok(
302
+ pageSummary("hardware device(s)", result?.records?.length ?? 0, result?.pageInfo),
303
+ result
304
+ );
305
+ })
306
+ );
307
+ server.registerTool(
308
+ "list_actions",
309
+ {
310
+ title: "List actions (test steps)",
311
+ description: "List available actions (steps) you can use to build a test \u2014 e.g. 'Power On Charger', 'Plug In Connector', 'Charge a Vehicle'. Filter by protocol. Each action lists its arguments: the name, whether it is required (allowNull false), and any default.",
312
+ inputSchema: {
313
+ protocol: protocolEnum.optional(),
314
+ search: z3.string().optional().describe("Filter by action name"),
315
+ ...paginationArgs,
316
+ project: z3.string().optional().describe("Project name or id (defaults to active project)")
317
+ }
318
+ },
319
+ async ({ protocol, search, page, pageSize, project }) => guard(async () => {
320
+ const ctx = await resolveSimWorld(project);
321
+ const result = await sdk().actions.list({
322
+ simWorldId: ctx.simWorldId,
323
+ searchValue: search,
324
+ filters: { isSystem: true, deviceType: "CHARGER", ...protocol ? { protocol } : {} },
325
+ pagination: pagination(page, pageSize, 100)
326
+ });
327
+ const actions = (result?.records ?? []).map((a) => ({
328
+ name: a?.name,
329
+ description: a?.description ?? void 0,
330
+ args: summarizeArgs(a?.params)
331
+ }));
332
+ return ok(pageSummary("action(s)", actions.length, result?.pageInfo), actions);
333
+ })
334
+ );
335
+ }
336
+
337
+ // src/tools/simulators.ts
338
+ import { z as z4 } from "zod";
339
+ async function runOnSimulator(project, simulator, actionName, args = []) {
340
+ const ctx = await resolveSimWorld(project);
341
+ const sim = await resolveSimulator(ctx.simWorldId, simulator);
342
+ const actionId = await resolveActionId(ctx.simWorldId, sim.protocol, actionName);
343
+ const status = await sdk().testRuns.runAction({
344
+ chargerId: sim.id,
345
+ action: { requirementKey: "1", stream: 1, order: 1, actionId, args }
346
+ });
347
+ return { sim, actionName, status };
348
+ }
349
+ function registerSimulatorTools(server) {
350
+ const projectArg = z4.string().optional().describe("Project name or id (defaults to active project)");
351
+ server.registerTool(
352
+ "power_on_simulator",
353
+ {
354
+ title: "Power on simulator",
355
+ description: "Power on a simulator (boots it and connects it to the OCPP server).",
356
+ inputSchema: {
357
+ simulator: z4.string().describe("Simulator id or base name"),
358
+ project: projectArg
359
+ }
360
+ },
361
+ async ({ simulator, project }) => guard(async () => {
362
+ const { sim, status } = await runOnSimulator(project, simulator, "Power On Charger");
363
+ return ok(`Powered on ${sim.baseName} (id: ${sim.id}).`, { status });
364
+ })
365
+ );
366
+ server.registerTool(
367
+ "power_off_simulator",
368
+ {
369
+ title: "Power off simulator",
370
+ description: "Power off a simulator.",
371
+ inputSchema: {
372
+ simulator: z4.string().describe("Simulator id or base name"),
373
+ project: projectArg
374
+ }
375
+ },
376
+ async ({ simulator, project }) => guard(async () => {
377
+ const { sim, status } = await runOnSimulator(project, simulator, "Power Off Charger");
378
+ return ok(`Powered off ${sim.baseName} (id: ${sim.id}).`, { status });
379
+ })
380
+ );
381
+ server.registerTool(
382
+ "run_simulator_action",
383
+ {
384
+ title: "Run simulator action",
385
+ description: "Run a single named action against a simulator \u2014 e.g. 'Reset Charger', 'Plug In Connector', 'Plug Out Connector'. Use list_actions to discover names and required args. Args are passed as key/value pairs (e.g. connectorId=1).",
386
+ inputSchema: {
387
+ simulator: z4.string().describe("Simulator id or base name"),
388
+ actionName: z4.string().describe("Exact action name, e.g. 'Reset Charger'"),
389
+ args: z4.array(z4.object({ key: z4.string(), value: z4.string() })).optional().describe("Action arguments as key/value pairs"),
390
+ project: projectArg
391
+ }
392
+ },
393
+ async ({ simulator, actionName, args, project }) => guard(async () => {
394
+ const { sim, status } = await runOnSimulator(project, simulator, actionName, args ?? []);
395
+ return ok(`Ran "${actionName}" on ${sim.baseName} (id: ${sim.id}).`, { status });
396
+ })
397
+ );
398
+ }
399
+
400
+ // src/tools/tests.ts
401
+ import { z as z5 } from "zod";
402
+ var protocolEnum2 = z5.enum(["OCPP_1_6", "OCPP_2_0_1"]);
403
+ function registerTestTools(server) {
404
+ const projectArg = z5.string().optional().describe("Project name or id (defaults to active project)");
405
+ server.registerTool(
406
+ "list_tests",
407
+ {
408
+ title: "List tests",
409
+ description: "List tests (scenarios) in the active project's sim world.",
410
+ inputSchema: {
411
+ protocol: protocolEnum2.optional(),
412
+ search: z5.string().optional().describe("Filter by test name"),
413
+ ...paginationArgs,
414
+ project: projectArg
415
+ }
416
+ },
417
+ async ({ protocol, search, page, pageSize, project }) => guard(async () => {
418
+ const ctx = await resolveSimWorld(project);
419
+ const result = await sdk().tests.list({
420
+ simWorldId: ctx.simWorldId,
421
+ searchValue: search,
422
+ filters: protocol ? { protocol } : void 0,
423
+ pagination: pagination(page, pageSize)
424
+ });
425
+ return ok(pageSummary("test(s)", result?.records?.length ?? 0, result?.pageInfo), result);
426
+ })
427
+ );
428
+ server.registerTool(
429
+ "get_test",
430
+ {
431
+ title: "Get test",
432
+ description: "Get a test by id, including its device requirements.",
433
+ inputSchema: {
434
+ testId: z5.string().describe("Test (scenario) id")
435
+ }
436
+ },
437
+ async ({ testId }) => guard(async () => {
438
+ const test = await sdk().tests.get(testId);
439
+ if (!test) return ok(`No test found with id ${testId}.`);
440
+ return ok(`Test ${test.name} (id: ${testId}).`, test);
441
+ })
442
+ );
443
+ server.registerTool(
444
+ "create_test",
445
+ {
446
+ title: "Create test",
447
+ description: "Create a test from an ordered list of steps. Each step references an action by name (use list_actions to discover them) plus optional key/value args. The steps run in order against a single simulator. Example steps: 'Power On Charger', 'Plug In Connector', 'Use RFID Card', 'Charge a Vehicle', 'Plug Out Connector', 'Power Off Charger'.",
448
+ inputSchema: {
449
+ name: z5.string().describe("Test name"),
450
+ protocol: protocolEnum2,
451
+ steps: z5.array(
452
+ z5.object({
453
+ actionName: z5.string().describe("Exact action name, e.g. 'Power On Charger'"),
454
+ args: z5.array(z5.object({ key: z5.string(), value: z5.string() })).optional().describe("Action arguments as key/value pairs")
455
+ })
456
+ ).min(1).describe("Ordered list of steps"),
457
+ project: projectArg
458
+ }
459
+ },
460
+ async ({ name, protocol, steps, project }) => guard(async () => {
461
+ const ctx = await resolveSimWorld(project);
462
+ const actions = [];
463
+ for (let i = 0; i < steps.length; i++) {
464
+ const actionId = await resolveActionId(ctx.simWorldId, protocol, steps[i].actionName);
465
+ actions.push({
466
+ order: i + 1,
467
+ stream: 1,
468
+ actionId,
469
+ requirementKey: "charger",
470
+ args: steps[i].args ?? []
471
+ });
472
+ }
473
+ const test = await sdk().tests.create({
474
+ simWorldId: ctx.simWorldId,
475
+ name,
476
+ deviceType: "CHARGER",
477
+ requirements: [
478
+ { key: "charger", type: "CHARGER", params: [{ key: "protocol", value: protocol }] }
479
+ ],
480
+ actions
481
+ });
482
+ return ok(`Created test "${name}" (id: ${test?.id}).`, test);
483
+ })
484
+ );
485
+ }
486
+
487
+ // src/tools/test-runs.ts
488
+ import { z as z6 } from "zod";
489
+ function registerTestRunTools(server) {
490
+ const projectArg = z6.string().optional().describe("Project name or id (defaults to active project)");
491
+ server.registerTool(
492
+ "run_test",
493
+ {
494
+ title: "Run test",
495
+ description: "Run a test (scenario) on a simulator, starting immediately. Returns the test run id and status \u2014 poll with get_test_run to follow it. A simulator runs one test at a time; if it is busy this says so and starts nothing.",
496
+ inputSchema: {
497
+ testId: z6.string().describe("Test (scenario) id to run"),
498
+ simulator: z6.string().describe("Simulator id or base name to run the test on"),
499
+ project: projectArg
500
+ }
501
+ },
502
+ async ({ testId, simulator, project }) => guard(async () => {
503
+ const ctx = await resolveSimWorld(project);
504
+ const test = await sdk().tests.get(testId);
505
+ const requirementId = test?.requirements?.[0]?.id;
506
+ if (!requirementId) return fail(`Test ${testId} has no device requirement to assign.`);
507
+ const sim = await resolveSimulator(ctx.simWorldId, simulator);
508
+ const run = await sdk().testRuns.create({
509
+ scenarioId: testId,
510
+ simWorldId: ctx.simWorldId,
511
+ scheduleMode: "NOW",
512
+ speedup: 1,
513
+ forceDelete: false,
514
+ requirementDevices: [
515
+ { requirementId, deviceId: sim.id, deviceMode: "SIMULATOR" }
516
+ ]
517
+ }).catch(async (err) => {
518
+ const message = err instanceof Error ? err.message : String(err);
519
+ if (!/conflict/i.test(message)) throw err;
520
+ const details = await sdk().simulators.get(sim.id).catch(() => null);
521
+ const blocking = details?.lastRun;
522
+ throw new Error(
523
+ `${sim.baseName} (id: ${sim.id}) is busy` + (blocking?.id ? ` with test run ${blocking.id} (${blocking.status})` : "") + `, so nothing was started. Stop that run with stop_test_run \u2014 the simulator is free once it stops \u2014 then run this test again.`
524
+ );
525
+ });
526
+ return ok(
527
+ `Started test "${test?.name}" on ${sim.baseName} \u2014 run id ${run?.id}, status ${run?.status}.`,
528
+ run
529
+ );
530
+ })
531
+ );
532
+ server.registerTool(
533
+ "get_test_run",
534
+ {
535
+ title: "Get test run",
536
+ description: "Get a test run's status by id.",
537
+ inputSchema: {
538
+ runId: z6.string().describe("Test run id")
539
+ }
540
+ },
541
+ async ({ runId }) => guard(async () => {
542
+ const run = await sdk().testRuns.get(runId);
543
+ if (!run) return ok(`No test run found with id ${runId}.`);
544
+ const reason = run.failReason ? ` \u2014 ${run.failReason}` : "";
545
+ return ok(`Test run ${runId} ("${run.scenario?.name}"): ${run.status}${reason}.`, run);
546
+ })
547
+ );
548
+ server.registerTool(
549
+ "list_test_runs",
550
+ {
551
+ title: "List test runs",
552
+ description: "List recent test runs in the active project's sim world.",
553
+ inputSchema: {
554
+ ...paginationArgs,
555
+ project: projectArg
556
+ }
557
+ },
558
+ async ({ page, pageSize, project }) => guard(async () => {
559
+ const ctx = await resolveSimWorld(project);
560
+ const result = await sdk().testRuns.list({
561
+ simWorldId: ctx.simWorldId,
562
+ pagination: pagination(page, pageSize)
563
+ });
564
+ return ok(pageSummary("test run(s)", result?.records?.length ?? 0, result?.pageInfo), result);
565
+ })
566
+ );
567
+ server.registerTool(
568
+ "stop_test_run",
569
+ {
570
+ title: "Stop test run",
571
+ description: "Shut down (stop) a running or scheduled test run by id.",
572
+ inputSchema: {
573
+ runId: z6.string().describe("Test run id")
574
+ }
575
+ },
576
+ async ({ runId }) => guard(async () => {
577
+ const existing = await sdk().testRuns.get(runId);
578
+ if (!existing) return fail(`No test run found with id ${runId}.`);
579
+ await sdk().testRuns.shutDown(runId);
580
+ const after = await sdk().testRuns.get(runId);
581
+ return ok(
582
+ `Shut down test run ${runId} (was ${existing.status}, now ${after?.status ?? "unknown"}).`,
583
+ after
584
+ );
585
+ })
586
+ );
587
+ }
588
+
589
+ // src/index.ts
590
+ async function main() {
591
+ if (!process.env.TIMETICK_API_TOKEN) {
592
+ console.error(
593
+ "TIMETICK_API_TOKEN is not set. Generate an API key in the Timetick portal (Account \u2192 API keys) and expose it to the MCP server as TIMETICK_API_TOKEN."
594
+ );
595
+ process.exit(1);
596
+ }
597
+ const server = new McpServer({ name: "timetick", version: "0.1.0" });
598
+ registerContextTools(server);
599
+ registerDataTools(server);
600
+ registerSimulatorTools(server);
601
+ registerTestTools(server);
602
+ registerTestRunTools(server);
603
+ const transport = new StdioServerTransport();
604
+ await server.connect(transport);
605
+ console.error("Timetick MCP server running on stdio");
606
+ }
607
+ main().catch((error) => {
608
+ console.error("Fatal error starting Timetick MCP server:", error);
609
+ process.exit(1);
610
+ });
611
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/tools/context.ts","../src/sdk.ts","../src/context.ts","../src/result.ts","../src/tools/data.ts","../src/resolve.ts","../src/pagination.ts","../src/tools/simulators.ts","../src/tools/tests.ts","../src/tools/test-runs.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport { registerContextTools } from \"./tools/context.js\";\nimport { registerDataTools } from \"./tools/data.js\";\nimport { registerSimulatorTools } from \"./tools/simulators.js\";\nimport { registerTestTools } from \"./tools/tests.js\";\nimport { registerTestRunTools } from \"./tools/test-runs.js\";\n\nasync function main() {\n if (!process.env.TIMETICK_API_TOKEN) {\n console.error(\n \"TIMETICK_API_TOKEN is not set. Generate an API key in the Timetick portal \" +\n \"(Account → API keys) and expose it to the MCP server as TIMETICK_API_TOKEN.\",\n );\n process.exit(1);\n }\n\n const server = new McpServer({ name: \"timetick\", version: \"0.1.0\" });\n\n registerContextTools(server);\n registerDataTools(server);\n registerSimulatorTools(server);\n registerTestTools(server);\n registerTestRunTools(server);\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error(\"Timetick MCP server running on stdio\");\n}\n\nmain().catch((error) => {\n console.error(\"Fatal error starting Timetick MCP server:\", error);\n process.exit(1);\n});\n","import { z } from \"zod\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { getContext, useProject, peekContext } from \"../context.js\";\nimport { ok, guard } from \"../result.js\";\n\nexport function registerContextTools(server: McpServer) {\n server.registerTool(\n \"get_context\",\n {\n title: \"Get active context\",\n description:\n \"Show the project and sim world the MCP server is currently operating on. \" +\n \"If no project is selected yet and you have only one project, it is auto-selected.\",\n inputSchema: {},\n },\n async () =>\n guard(async () => {\n const ctx = peekContext() ?? (await getContext());\n return ok(\n `Active project: ${ctx.projectName} (id: ${ctx.projectId}); sim world: ${ctx.simWorldId} [${ctx.source}]`,\n ctx,\n );\n }),\n );\n\n server.registerTool(\n \"use_project\",\n {\n title: \"Select project\",\n description:\n \"Switch the active project by name or id. Only needed if you have more than one project. \" +\n \"Caches the project's sim world for subsequent tools.\",\n inputSchema: {\n project: z.string().describe(\"Project name or id to make active\"),\n },\n },\n async ({ project }) =>\n guard(async () => {\n const ctx = await useProject({ project });\n return ok(`Active project is now ${ctx.projectName} (id: ${ctx.projectId}).`, ctx);\n }),\n );\n}\n","import { createClient, type TimetickClient } from \"@timetick/sdk\";\n\nlet client: TimetickClient | null = null;\n\n/** Lazily-created singleton Timetick SDK client, authenticated from the environment. */\nexport function sdk(): TimetickClient {\n if (client) return client;\n\n const apiKey = process.env.TIMETICK_API_TOKEN;\n if (!apiKey) {\n throw new Error(\n \"TIMETICK_API_TOKEN is not set. Generate an API key in the Timetick portal \" +\n \"(Account → API keys) and expose it to the MCP server as TIMETICK_API_TOKEN.\",\n );\n }\n\n client = createClient({ apiKey, endpoint: process.env.TIMETICK_API_URL });\n return client;\n}\n","import { sdk } from \"./sdk.js\";\n\nexport interface Context {\n projectId: string;\n projectName: string;\n simWorldId: string;\n /** Scope id used by hardware/device queries — distinct from simWorldId. */\n deviceScopeId: string;\n /** How the project was chosen — surfaced to the user for transparency. */\n source: \"single-project\" | \"explicit\";\n}\n\n/** Thrown when the project/sim world can't be resolved without user input. */\nexport class ContextError extends Error {}\n\nlet cached: Context | null = null;\n\n/** Resolves a project's sim world (one sim world per project today). */\nasync function buildContext(\n projectId: string,\n projectName: string,\n source: Context[\"source\"],\n): Promise<Context> {\n const worlds = await sdk().simWorlds.list(projectId);\n const world = worlds?.[0];\n if (!world?.id) {\n throw new ContextError(`Project \"${projectName}\" has no sim world to operate on.`);\n }\n return {\n projectId,\n projectName,\n simWorldId: world.id,\n deviceScopeId: world.deviceScopeId ?? world.id,\n source,\n };\n}\n\nconst PROJECT_LIST_LIMIT = 15;\n\ntype ProjectSummary = { id?: string | null; name?: string | null } | null | undefined;\n\nfunction formatProjects(projects: readonly ProjectSummary[] | null | undefined): string {\n const all = projects ?? [];\n const lines = all\n .slice(0, PROJECT_LIST_LIMIT)\n .map((p) => ` - ${p?.name} (id: ${p?.id})`);\n if (all.length > PROJECT_LIST_LIMIT) {\n lines.push(` …and ${all.length - PROJECT_LIST_LIMIT} more — call list_projects to see them all.`);\n }\n return lines.join(\"\\n\");\n}\n\nasync function findProject(project: string): Promise<{ id: string; name: string }> {\n const projects = await sdk().projects.list();\n const needle = project.toLowerCase();\n const match = projects?.find(\n (p) => p?.id === project || p?.name?.toLowerCase() === needle,\n );\n if (!match?.id) {\n throw new ContextError(\n `No project matching \"${project}\". Available:\\n${formatProjects(projects)}`,\n );\n }\n return { id: match.id, name: match.name ?? \"(unnamed)\" };\n}\n\n/**\n * Returns the active context, auto-selecting the project when there is only one.\n * Throws ContextError (with the project list) when the choice is ambiguous.\n */\nexport async function getContext(): Promise<Context> {\n if (cached) return cached;\n\n const projects = await sdk().projects.list();\n if (!projects || projects.length === 0) {\n throw new ContextError(\"No projects are available for this API key.\");\n }\n if (projects.length > 1) {\n throw new ContextError(\n `You have ${projects.length} projects. Call \\`use_project\\` with a project name or id ` +\n `first:\\n${formatProjects(projects)}`,\n );\n }\n\n const only = projects[0]!;\n cached = await buildContext(only.id!, only.name ?? \"(unnamed)\", \"single-project\");\n return cached;\n}\n\n/** Explicitly switches the active project by name or id, and caches its sim world. */\nexport async function useProject(input: { project: string }): Promise<Context> {\n const match = await findProject(input.project);\n cached = await buildContext(match.id, match.name, \"explicit\");\n return cached;\n}\n\nexport async function lookupProject(project: string): Promise<Context> {\n const match = await findProject(project);\n return buildContext(match.id, match.name, \"explicit\");\n}\n\n/** Returns the cached context without resolving, if any. */\nexport function peekContext(): Context | null {\n return cached;\n}\n","import { TimetickError } from \"@timetick/sdk\";\n\ntype ToolResult = {\n content: { type: \"text\"; text: string }[];\n isError?: boolean;\n};\n\n/** Wraps a value into an MCP text result, pretty-printing objects as JSON. */\nexport function ok(text: string, data?: unknown): ToolResult {\n const body = data === undefined ? text : `${text}\\n\\n${JSON.stringify(data, null, 2)}`;\n return { content: [{ type: \"text\", text: body }] };\n}\n\nexport function fail(message: string): ToolResult {\n return { content: [{ type: \"text\", text: message }], isError: true };\n}\n\n/** Runs a tool handler, normalizing SDK/unknown errors into an isError result. */\nexport async function guard(fn: () => Promise<ToolResult>): Promise<ToolResult> {\n try {\n return await fn();\n } catch (err) {\n if (err instanceof TimetickError) return fail(`Timetick API error: ${err.message}`);\n return fail(err instanceof Error ? err.message : String(err));\n }\n}\n","import { z } from \"zod\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { sdk } from \"../sdk.js\";\nimport { resolveSimWorld, resolveSimulator } from \"../resolve.js\";\nimport { paginationArgs, pagination, pageSummary } from \"../pagination.js\";\nimport { ok, guard } from \"../result.js\";\n\nconst protocolEnum = z.enum([\"OCPP_1_6\", \"OCPP_2_0_1\"]);\n\nfunction summarizeArgs(params: unknown): Record<string, string> | undefined {\n if (!params || typeof params !== \"object\") return undefined;\n const summary: Record<string, string> = {};\n for (const [name, spec] of Object.entries(params as Record<string, unknown>)) {\n if (!spec || typeof spec !== \"object\") continue;\n const { type, allowNull, defaultValue, visible } = spec as Record<string, unknown>;\n if (visible === false) continue;\n const parts = [String(type ?? \"unknown\"), allowNull === false ? \"required\" : \"optional\"];\n if (defaultValue !== undefined) parts.push(`default ${JSON.stringify(defaultValue)}`);\n summary[name] = parts.join(\", \");\n }\n return Object.keys(summary).length > 0 ? summary : undefined;\n}\n\nexport function registerDataTools(server: McpServer) {\n server.registerTool(\n \"list_projects\",\n {\n title: \"List projects\",\n description: \"List all projects available to your API key.\",\n inputSchema: {},\n },\n async () =>\n guard(async () => {\n const projects = await sdk().projects.list();\n return ok(`Found ${projects?.length ?? 0} project(s).`, projects);\n }),\n );\n\n server.registerTool(\n \"list_simulators\",\n {\n title: \"List simulators\",\n description:\n \"List simulators (virtual chargers) in the active project's sim world. \" +\n \"Optionally filter by name search or protocol.\",\n inputSchema: {\n search: z.string().optional().describe(\"Filter by simulator base name\"),\n protocol: protocolEnum.optional(),\n ...paginationArgs,\n project: z.string().optional().describe(\"Project name or id (defaults to active project)\"),\n },\n },\n async ({ search, protocol, page, pageSize, project }) =>\n guard(async () => {\n const ctx = await resolveSimWorld(project);\n const result = await sdk().simulators.list({\n simWorldId: ctx.simWorldId,\n searchValue: search,\n filters: protocol ? { protocol } : undefined,\n pagination: pagination(page, pageSize),\n });\n return ok(\n pageSummary(\"simulator(s)\", result?.records?.length ?? 0, result?.pageInfo),\n result,\n );\n }),\n );\n\n server.registerTool(\n \"get_simulator\",\n {\n title: \"Get simulator\",\n description:\n \"Get a simulator's details and live status (network status, launched/power status, connectors) \" +\n \"by id or base name.\",\n inputSchema: {\n simulator: z.string().describe(\"Simulator id or base name\"),\n project: z.string().optional().describe(\"Project name or id (defaults to active project)\"),\n },\n },\n async ({ simulator, project }) =>\n guard(async () => {\n const ctx = await resolveSimWorld(project);\n const resolved = await resolveSimulator(ctx.simWorldId, simulator);\n const details = await sdk().simulators.get(resolved.id);\n return ok(`Simulator ${resolved.baseName} (id: ${resolved.id}).`, details);\n }),\n );\n\n server.registerTool(\n \"list_hardware\",\n {\n title: \"List hardware\",\n description: \"List physical OCPP hardware devices in the active project's sim world.\",\n inputSchema: {\n search: z.string().optional().describe(\"Filter by serial number / model\"),\n ...paginationArgs,\n project: z.string().optional().describe(\"Project name or id (defaults to active project)\"),\n },\n },\n async ({ search, page, pageSize, project }) =>\n guard(async () => {\n const ctx = await resolveSimWorld(project);\n const result = await sdk().hardware.list({\n deviceScopeId: ctx.deviceScopeId,\n searchValue: search,\n pagination: pagination(page, pageSize),\n });\n return ok(\n pageSummary(\"hardware device(s)\", result?.records?.length ?? 0, result?.pageInfo),\n result,\n );\n }),\n );\n\n server.registerTool(\n \"list_actions\",\n {\n title: \"List actions (test steps)\",\n description:\n \"List available actions (steps) you can use to build a test — e.g. 'Power On Charger', \" +\n \"'Plug In Connector', 'Charge a Vehicle'. Filter by protocol. Each action lists its \" +\n \"arguments: the name, whether it is required (allowNull false), and any default.\",\n inputSchema: {\n protocol: protocolEnum.optional(),\n search: z.string().optional().describe(\"Filter by action name\"),\n ...paginationArgs,\n project: z.string().optional().describe(\"Project name or id (defaults to active project)\"),\n },\n },\n async ({ protocol, search, page, pageSize, project }) =>\n guard(async () => {\n const ctx = await resolveSimWorld(project);\n const result = await sdk().actions.list({\n simWorldId: ctx.simWorldId,\n searchValue: search,\n filters: { isSystem: true, deviceType: \"CHARGER\", ...(protocol ? { protocol } : {}) },\n pagination: pagination(page, pageSize, 100),\n });\n const actions = (result?.records ?? []).map((a) => ({\n name: a?.name,\n description: a?.description ?? undefined,\n args: summarizeArgs(a?.params),\n }));\n return ok(pageSummary(\"action(s)\", actions.length, result?.pageInfo), actions);\n }),\n );\n}\n","import { sdk } from \"./sdk.js\";\nimport { getContext, lookupProject, type Context } from \"./context.js\";\n\n/** Resolves the sim world to operate in: explicit project arg wins, else session context. */\nexport async function resolveSimWorld(project?: string): Promise<Context> {\n if (project) return lookupProject(project);\n return getContext();\n}\n\nexport interface ResolvedSimulator {\n id: string;\n baseName: string;\n protocol: string;\n}\n\n/** Resolves a simulator by id or by (case-insensitive) base name within a sim world. */\nexport async function resolveSimulator(\n simWorldId: string,\n simulator: string,\n): Promise<ResolvedSimulator> {\n const needle = simulator.toLowerCase();\n const page = await sdk().simulators.list({ simWorldId, searchValue: simulator });\n const records = page?.records ?? [];\n\n const exact =\n records.find((r) => r?.id === simulator) ??\n records.find((r) => r?.baseName?.toLowerCase() === needle) ??\n (records.length === 1 ? records[0] : undefined);\n\n if (exact?.id) {\n return { id: exact.id, baseName: exact.baseName ?? \"\", protocol: String(exact.protocol) };\n }\n\n // Fall back to treating the input as a raw id.\n const byId = await sdk().simulators.get(simulator).catch(() => null);\n if (byId?.id) {\n return { id: byId.id, baseName: byId.baseName ?? \"\", protocol: String(byId.protocol) };\n }\n\n const candidates = records.map((r) => ` - ${r?.baseName} (id: ${r?.id})`).join(\"\\n\");\n throw new Error(\n `No simulator matching \"${simulator}\".` +\n (candidates ? `\\nCandidates:\\n${candidates}` : \" No simulators found in this sim world.\"),\n );\n}\n\n/** Finds a system action by exact name for a given protocol; returns its id. */\nexport async function resolveActionId(\n simWorldId: string,\n protocol: string,\n actionName: string,\n): Promise<string> {\n const page = await sdk().actions.list({\n simWorldId,\n searchValue: actionName,\n filters: { isSystem: true, protocol: protocol as any, deviceType: \"CHARGER\" },\n pagination: { page: 1, pageSize: 50 },\n });\n const records = page?.records ?? [];\n const needle = actionName.toLowerCase();\n const match =\n records.find((a) => a?.name?.toLowerCase() === needle) ??\n records.find((a) => a?.name?.toLowerCase().includes(needle));\n\n if (!match?.id) {\n const available = records.map((a) => ` - ${a?.name}`).join(\"\\n\");\n throw new Error(\n `Action \"${actionName}\" not found for ${protocol}.` +\n (available ? `\\nAvailable system actions matching your search:\\n${available}` : \"\"),\n );\n }\n return match.id;\n}\n","import { z } from \"zod\";\n\nexport const DEFAULT_PAGE_SIZE = 20;\n\nexport const paginationArgs = {\n page: z.number().int().min(1).optional().describe(\"Page number, starting at 1 (default 1)\"),\n pageSize: z\n .number()\n .int()\n .min(1)\n .max(200)\n .optional()\n .describe(`Results per page (default ${DEFAULT_PAGE_SIZE})`),\n};\n\nexport function pagination(page?: number, pageSize?: number, defaultPageSize = DEFAULT_PAGE_SIZE) {\n return { page: page ?? 1, pageSize: pageSize ?? defaultPageSize };\n}\n\ninterface PageInfoLike {\n page?: number | null;\n pageSize?: number | null;\n hasNextPage?: boolean | null;\n totalCount?: number | null;\n}\n\nexport function pageSummary(noun: string, count: number, pageInfo?: PageInfoLike | null): string {\n const total = pageInfo?.totalCount;\n const head =\n total != null && total !== count\n ? `Showing ${count} of ${total} ${noun} (page ${pageInfo?.page ?? 1}).`\n : `Found ${count} ${noun}.`;\n return pageInfo?.hasNextPage ? `${head} More results available — request the next page.` : head;\n}\n","import { z } from \"zod\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { sdk } from \"../sdk.js\";\nimport { resolveSimWorld, resolveSimulator, resolveActionId } from \"../resolve.js\";\nimport { ok, guard } from \"../result.js\";\n\n/** Runs a single named action against a simulator via the SDK's runAction. */\nasync function runOnSimulator(\n project: string | undefined,\n simulator: string,\n actionName: string,\n args: { key: string; value: string }[] = [],\n) {\n const ctx = await resolveSimWorld(project);\n const sim = await resolveSimulator(ctx.simWorldId, simulator);\n const actionId = await resolveActionId(ctx.simWorldId, sim.protocol, actionName);\n\n const status = await sdk().testRuns.runAction({\n chargerId: sim.id,\n action: { requirementKey: \"1\", stream: 1, order: 1, actionId, args },\n });\n return { sim, actionName, status };\n}\n\nexport function registerSimulatorTools(server: McpServer) {\n const projectArg = z\n .string()\n .optional()\n .describe(\"Project name or id (defaults to active project)\");\n\n server.registerTool(\n \"power_on_simulator\",\n {\n title: \"Power on simulator\",\n description: \"Power on a simulator (boots it and connects it to the OCPP server).\",\n inputSchema: {\n simulator: z.string().describe(\"Simulator id or base name\"),\n project: projectArg,\n },\n },\n async ({ simulator, project }) =>\n guard(async () => {\n const { sim, status } = await runOnSimulator(project, simulator, \"Power On Charger\");\n return ok(`Powered on ${sim.baseName} (id: ${sim.id}).`, { status });\n }),\n );\n\n server.registerTool(\n \"power_off_simulator\",\n {\n title: \"Power off simulator\",\n description: \"Power off a simulator.\",\n inputSchema: {\n simulator: z.string().describe(\"Simulator id or base name\"),\n project: projectArg,\n },\n },\n async ({ simulator, project }) =>\n guard(async () => {\n const { sim, status } = await runOnSimulator(project, simulator, \"Power Off Charger\");\n return ok(`Powered off ${sim.baseName} (id: ${sim.id}).`, { status });\n }),\n );\n\n server.registerTool(\n \"run_simulator_action\",\n {\n title: \"Run simulator action\",\n description:\n \"Run a single named action against a simulator — e.g. 'Reset Charger', 'Plug In Connector', \" +\n \"'Plug Out Connector'. Use list_actions to discover names and required args. \" +\n \"Args are passed as key/value pairs (e.g. connectorId=1).\",\n inputSchema: {\n simulator: z.string().describe(\"Simulator id or base name\"),\n actionName: z.string().describe(\"Exact action name, e.g. 'Reset Charger'\"),\n args: z\n .array(z.object({ key: z.string(), value: z.string() }))\n .optional()\n .describe(\"Action arguments as key/value pairs\"),\n project: projectArg,\n },\n },\n async ({ simulator, actionName, args, project }) =>\n guard(async () => {\n const { sim, status } = await runOnSimulator(project, simulator, actionName, args ?? []);\n return ok(`Ran \"${actionName}\" on ${sim.baseName} (id: ${sim.id}).`, { status });\n }),\n );\n}\n","import { z } from \"zod\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { sdk } from \"../sdk.js\";\nimport { resolveSimWorld, resolveActionId } from \"../resolve.js\";\nimport { paginationArgs, pagination, pageSummary } from \"../pagination.js\";\nimport { ok, guard } from \"../result.js\";\n\nconst protocolEnum = z.enum([\"OCPP_1_6\", \"OCPP_2_0_1\"]);\n\nexport function registerTestTools(server: McpServer) {\n const projectArg = z\n .string()\n .optional()\n .describe(\"Project name or id (defaults to active project)\");\n\n server.registerTool(\n \"list_tests\",\n {\n title: \"List tests\",\n description: \"List tests (scenarios) in the active project's sim world.\",\n inputSchema: {\n protocol: protocolEnum.optional(),\n search: z.string().optional().describe(\"Filter by test name\"),\n ...paginationArgs,\n project: projectArg,\n },\n },\n async ({ protocol, search, page, pageSize, project }) =>\n guard(async () => {\n const ctx = await resolveSimWorld(project);\n const result = await sdk().tests.list({\n simWorldId: ctx.simWorldId,\n searchValue: search,\n filters: protocol ? { protocol } : undefined,\n pagination: pagination(page, pageSize),\n });\n return ok(pageSummary(\"test(s)\", result?.records?.length ?? 0, result?.pageInfo), result);\n }),\n );\n\n server.registerTool(\n \"get_test\",\n {\n title: \"Get test\",\n description: \"Get a test by id, including its device requirements.\",\n inputSchema: {\n testId: z.string().describe(\"Test (scenario) id\"),\n },\n },\n async ({ testId }) =>\n guard(async () => {\n const test = await sdk().tests.get(testId);\n if (!test) return ok(`No test found with id ${testId}.`);\n return ok(`Test ${test.name} (id: ${testId}).`, test);\n }),\n );\n\n server.registerTool(\n \"create_test\",\n {\n title: \"Create test\",\n description:\n \"Create a test from an ordered list of steps. Each step references an action by name \" +\n \"(use list_actions to discover them) plus optional key/value args. The steps run in order \" +\n \"against a single simulator. Example steps: 'Power On Charger', 'Plug In Connector', \" +\n \"'Use RFID Card', 'Charge a Vehicle', 'Plug Out Connector', 'Power Off Charger'.\",\n inputSchema: {\n name: z.string().describe(\"Test name\"),\n protocol: protocolEnum,\n steps: z\n .array(\n z.object({\n actionName: z.string().describe(\"Exact action name, e.g. 'Power On Charger'\"),\n args: z\n .array(z.object({ key: z.string(), value: z.string() }))\n .optional()\n .describe(\"Action arguments as key/value pairs\"),\n }),\n )\n .min(1)\n .describe(\"Ordered list of steps\"),\n project: projectArg,\n },\n },\n async ({ name, protocol, steps, project }) =>\n guard(async () => {\n const ctx = await resolveSimWorld(project);\n\n const actions = [];\n for (let i = 0; i < steps.length; i++) {\n const actionId = await resolveActionId(ctx.simWorldId, protocol, steps[i].actionName);\n actions.push({\n order: i + 1,\n stream: 1,\n actionId,\n requirementKey: \"charger\",\n args: steps[i].args ?? [],\n });\n }\n\n const test = await sdk().tests.create({\n simWorldId: ctx.simWorldId,\n name,\n deviceType: \"CHARGER\",\n requirements: [\n { key: \"charger\", type: \"CHARGER\", params: [{ key: \"protocol\", value: protocol }] },\n ],\n actions,\n });\n return ok(`Created test \"${name}\" (id: ${test?.id}).`, test);\n }),\n );\n}\n","import { z } from \"zod\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { sdk } from \"../sdk.js\";\nimport { resolveSimWorld, resolveSimulator } from \"../resolve.js\";\nimport { paginationArgs, pagination, pageSummary } from \"../pagination.js\";\nimport { ok, fail, guard } from \"../result.js\";\n\nexport function registerTestRunTools(server: McpServer) {\n const projectArg = z\n .string()\n .optional()\n .describe(\"Project name or id (defaults to active project)\");\n\n server.registerTool(\n \"run_test\",\n {\n title: \"Run test\",\n description:\n \"Run a test (scenario) on a simulator, starting immediately. \" +\n \"Returns the test run id and status — poll with get_test_run to follow it. \" +\n \"A simulator runs one test at a time; if it is busy this says so and starts nothing.\",\n inputSchema: {\n testId: z.string().describe(\"Test (scenario) id to run\"),\n simulator: z.string().describe(\"Simulator id or base name to run the test on\"),\n project: projectArg,\n },\n },\n async ({ testId, simulator, project }) =>\n guard(async () => {\n const ctx = await resolveSimWorld(project);\n const test = await sdk().tests.get(testId);\n const requirementId = test?.requirements?.[0]?.id;\n if (!requirementId) return fail(`Test ${testId} has no device requirement to assign.`);\n\n const sim = await resolveSimulator(ctx.simWorldId, simulator);\n const run = await sdk()\n .testRuns.create({\n scenarioId: testId,\n simWorldId: ctx.simWorldId,\n scheduleMode: \"NOW\",\n speedup: 1,\n forceDelete: false,\n requirementDevices: [\n { requirementId, deviceId: sim.id, deviceMode: \"SIMULATOR\" },\n ],\n })\n .catch(async (err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n if (!/conflict/i.test(message)) throw err;\n\n const details = await sdk().simulators.get(sim.id).catch(() => null);\n const blocking = details?.lastRun;\n throw new Error(\n `${sim.baseName} (id: ${sim.id}) is busy` +\n (blocking?.id ? ` with test run ${blocking.id} (${blocking.status})` : \"\") +\n `, so nothing was started. Stop that run with stop_test_run — the simulator is ` +\n `free once it stops — then run this test again.`,\n );\n });\n return ok(\n `Started test \"${test?.name}\" on ${sim.baseName} — run id ${run?.id}, status ${run?.status}.`,\n run,\n );\n }),\n );\n\n server.registerTool(\n \"get_test_run\",\n {\n title: \"Get test run\",\n description: \"Get a test run's status by id.\",\n inputSchema: {\n runId: z.string().describe(\"Test run id\"),\n },\n },\n async ({ runId }) =>\n guard(async () => {\n const run = await sdk().testRuns.get(runId);\n if (!run) return ok(`No test run found with id ${runId}.`);\n const reason = run.failReason ? ` — ${run.failReason}` : \"\";\n return ok(`Test run ${runId} (\"${run.scenario?.name}\"): ${run.status}${reason}.`, run);\n }),\n );\n\n server.registerTool(\n \"list_test_runs\",\n {\n title: \"List test runs\",\n description: \"List recent test runs in the active project's sim world.\",\n inputSchema: {\n ...paginationArgs,\n project: projectArg,\n },\n },\n async ({ page, pageSize, project }) =>\n guard(async () => {\n const ctx = await resolveSimWorld(project);\n const result = await sdk().testRuns.list({\n simWorldId: ctx.simWorldId,\n pagination: pagination(page, pageSize),\n });\n return ok(pageSummary(\"test run(s)\", result?.records?.length ?? 0, result?.pageInfo), result);\n }),\n );\n\n server.registerTool(\n \"stop_test_run\",\n {\n title: \"Stop test run\",\n description: \"Shut down (stop) a running or scheduled test run by id.\",\n inputSchema: {\n runId: z.string().describe(\"Test run id\"),\n },\n },\n async ({ runId }) =>\n guard(async () => {\n const existing = await sdk().testRuns.get(runId);\n if (!existing) return fail(`No test run found with id ${runId}.`);\n\n await sdk().testRuns.shutDown(runId);\n const after = await sdk().testRuns.get(runId);\n return ok(\n `Shut down test run ${runId} (was ${existing.status}, now ${after?.status ?? \"unknown\"}).`,\n after,\n );\n }),\n );\n}\n"],"mappings":"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","names":["z","z","z","z","z","z","protocolEnum","z","z","z"]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@timetick/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Model Context Protocol (MCP) server for Timetick (https://app.timetick.io) platform",
5
+ "license": "AGPL-3.0-or-later",
6
+ "author": "Timetick <support@timetick.io>",
7
+ "homepage": "https://timetick.io",
8
+ "bugs": {
9
+ "email": "support@timetick.io"
10
+ },
11
+ "keywords": [
12
+ "mcp",
13
+ "timetick",
14
+ "ocpp",
15
+ "ev-charging",
16
+ "claude",
17
+ "ai-agent",
18
+ "load-testing"
19
+ ],
20
+ "type": "module",
21
+ "bin": {
22
+ "timetick-mcp": "./dist/index.js"
23
+ },
24
+ "main": "./dist/index.js",
25
+ "files": [
26
+ "dist",
27
+ "README.md"
28
+ ],
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "registry": "https://registry.npmjs.org/"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "dev": "tsup --watch",
39
+ "typecheck": "tsc --noEmit",
40
+ "start": "node dist/index.js",
41
+ "inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.12.0",
45
+ "@timetick/sdk": "^2.0.1",
46
+ "graphql": "^16.0.0",
47
+ "zod": "^4.4.3"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.0.0",
51
+ "tsup": "^8.0.0",
52
+ "typescript": "^5.0.0"
53
+ }
54
+ }