tempo-api-mcp 2.0.0 → 2.0.2

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/dist/client.js CHANGED
@@ -4,16 +4,34 @@ import { fileURLToPath } from 'url';
4
4
  try {
5
5
  const { config } = await import('dotenv');
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
- config({ path: join(__dirname, '..', '.env'), override: false });
7
+ config({ path: join(__dirname, '..', '.env'), override: false, quiet: true });
8
8
  }
9
9
  catch {
10
10
  // not available — rely on process.env (mcpb sets credentials via mcp_config.env)
11
11
  }
12
+ /**
13
+ * Read an env var, trim whitespace, and treat as unset if blank or if the value
14
+ * looks like an unsubstituted shell placeholder (e.g. `${FOO}`) — defends
15
+ * against MCP hosts that pass .mcp.json env blocks through unexpanded.
16
+ */
17
+ function readVar(key) {
18
+ const raw = process.env[key];
19
+ if (typeof raw !== 'string')
20
+ return undefined;
21
+ const trimmed = raw.trim();
22
+ if (trimmed.length === 0)
23
+ return undefined;
24
+ if (trimmed === 'undefined' || trimmed === 'null')
25
+ return undefined;
26
+ if (/^\$\{[^}]*\}$/.test(trimmed))
27
+ return undefined;
28
+ return trimmed;
29
+ }
12
30
  const BASE_URL = 'https://api.tempo.io';
13
31
  export class TempoClient {
14
32
  apiToken;
15
33
  constructor() {
16
- const token = process.env.TEMPO_API_TOKEN;
34
+ const token = readVar('TEMPO_API_TOKEN');
17
35
  if (!token)
18
36
  throw new Error('TEMPO_API_TOKEN environment variable is required');
19
37
  this.apiToken = token;
package/dist/index.js CHANGED
@@ -1,54 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
4
  import { TempoClient } from './client.js';
6
- import { toolDefinitions as worklogTools, handleTool as handleWorklogs } from './tools/worklogs.js';
7
- import { toolDefinitions as planTools, handleTool as handlePlans } from './tools/plans.js';
8
- import { toolDefinitions as teamTools, handleTool as handleTeams } from './tools/teams.js';
9
- import { toolDefinitions as accountTools, handleTool as handleAccounts } from './tools/accounts.js';
10
- import { toolDefinitions as projectTools, handleTool as handleProjects } from './tools/projects.js';
5
+ import { register as registerWorklogs } from './tools/worklogs.js';
6
+ import { register as registerPlans } from './tools/plans.js';
7
+ import { register as registerTeams } from './tools/teams.js';
8
+ import { register as registerAccounts } from './tools/accounts.js';
9
+ import { register as registerProjects } from './tools/projects.js';
11
10
  const client = new TempoClient();
12
- const allTools = [
13
- ...worklogTools,
14
- ...planTools,
15
- ...teamTools,
16
- ...accountTools,
17
- ...projectTools,
18
- ];
19
- const handlers = {};
20
- for (const tool of worklogTools)
21
- handlers[tool.name] = (n, a) => handleWorklogs(n, a, client);
22
- for (const tool of planTools)
23
- handlers[tool.name] = (n, a) => handlePlans(n, a, client);
24
- for (const tool of teamTools)
25
- handlers[tool.name] = (n, a) => handleTeams(n, a, client);
26
- for (const tool of accountTools)
27
- handlers[tool.name] = (n, a) => handleAccounts(n, a, client);
28
- for (const tool of projectTools)
29
- handlers[tool.name] = (n, a) => handleProjects(n, a, client);
30
- const server = new Server({ name: 'tempo-api-mcp', version: '2.0.0' }, { capabilities: { tools: {} } });
31
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: allTools }));
32
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
33
- const { name, arguments: args = {} } = request.params;
34
- const handler = handlers[name];
35
- if (!handler) {
36
- return {
37
- content: [{ type: 'text', text: `Unknown tool: ${name}` }],
38
- isError: true,
39
- };
40
- }
41
- try {
42
- return await handler(name, args);
43
- }
44
- catch (err) {
45
- const message = err instanceof Error ? err.message : String(err);
46
- return {
47
- content: [{ type: 'text', text: `Error: ${message}` }],
48
- isError: true,
49
- };
50
- }
51
- });
11
+ const server = new McpServer({ name: 'tempo-api-mcp', version: '2.0.2' });
12
+ registerWorklogs(server, client);
13
+ registerPlans(server, client);
14
+ registerTeams(server, client);
15
+ registerAccounts(server, client);
16
+ registerProjects(server, client);
52
17
  console.error('[tempo-api-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.');
53
18
  const transport = new StdioServerTransport();
54
19
  await server.connect(transport);
@@ -1,183 +1,126 @@
1
- export const toolDefinitions = [
2
- {
3
- name: 'tempo_get_accounts',
1
+ import { z } from 'zod';
2
+ function buildAccountBody(args) {
3
+ const body = { key: args.key, name: args.name };
4
+ if (args.status !== undefined)
5
+ body.status = args.status;
6
+ if (args.leadAccountId !== undefined)
7
+ body.leadAccountId = args.leadAccountId;
8
+ if (args.categoryKey !== undefined)
9
+ body.categoryKey = args.categoryKey;
10
+ if (args.contactAccountId !== undefined)
11
+ body.contactAccountId = args.contactAccountId;
12
+ if (args.externalContactName !== undefined)
13
+ body.externalContactName = args.externalContactName;
14
+ if (args.monthlyBudget !== undefined)
15
+ body.monthlyBudget = args.monthlyBudget;
16
+ return body;
17
+ }
18
+ export function register(server, client) {
19
+ server.registerTool('tempo_get_accounts', {
4
20
  description: 'Retrieve a list of all Tempo accounts (OPEN and CLOSED).',
5
21
  annotations: { readOnlyHint: true },
6
22
  inputSchema: {
7
- type: 'object',
8
- properties: {
9
- offset: { type: 'integer', description: 'Pagination offset' },
10
- limit: { type: 'integer', description: 'Max results (default 50)' },
11
- },
12
- required: [],
23
+ offset: z.number().int().optional().describe('Pagination offset'),
24
+ limit: z.number().int().optional().describe('Max results (default 50)'),
13
25
  },
14
- },
15
- {
16
- name: 'tempo_get_account',
26
+ }, async ({ offset, limit }) => {
27
+ const data = await client.request('GET', '/4/accounts', undefined, { offset, limit });
28
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
29
+ });
30
+ server.registerTool('tempo_get_account', {
17
31
  description: 'Retrieve a single Tempo account by its key.',
18
32
  annotations: { readOnlyHint: true },
19
33
  inputSchema: {
20
- type: 'object',
21
- properties: {
22
- key: { type: 'string', description: 'Account key (e.g. ACCOUNT-123)' },
23
- },
24
- required: ['key'],
34
+ key: z.string().describe('Account key (e.g. ACCOUNT-123)'),
25
35
  },
26
- },
27
- {
28
- name: 'tempo_search_accounts',
36
+ }, async ({ key }) => {
37
+ const data = await client.request('GET', `/4/accounts/${key}`);
38
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
39
+ });
40
+ server.registerTool('tempo_search_accounts', {
29
41
  description: 'Search Tempo accounts with advanced filters (status, category, project).',
30
42
  annotations: { readOnlyHint: true },
31
43
  inputSchema: {
32
- type: 'object',
33
- properties: {
34
- query: { type: 'string', description: 'Text search across account name and key' },
35
- statusList: {
36
- type: 'array',
37
- items: { type: 'string', enum: ['OPEN', 'CLOSED', 'ARCHIVED'] },
38
- description: 'Filter by account status',
39
- },
40
- accountCategoryKeys: { type: 'array', items: { type: 'string' }, description: 'Filter by account category keys' },
41
- projectKeys: { type: 'array', items: { type: 'string' }, description: 'Filter by associated Jira project keys' },
42
- offset: { type: 'integer', description: 'Pagination offset' },
43
- limit: { type: 'integer', description: 'Max results (default 50)' },
44
- },
45
- required: [],
44
+ query: z.string().optional().describe('Text search across account name and key'),
45
+ statusList: z.array(z.enum(['OPEN', 'CLOSED', 'ARCHIVED'])).optional().describe('Filter by account status'),
46
+ accountCategoryKeys: z.array(z.string()).optional().describe('Filter by account category keys'),
47
+ projectKeys: z.array(z.string()).optional().describe('Filter by associated Jira project keys'),
48
+ offset: z.number().int().optional().describe('Pagination offset'),
49
+ limit: z.number().int().optional().describe('Max results (default 50)'),
46
50
  },
47
- },
48
- {
49
- name: 'tempo_create_account',
51
+ }, async ({ query, statusList, accountCategoryKeys, projectKeys, offset, limit }) => {
52
+ const qs = {};
53
+ if (offset !== undefined)
54
+ qs.offset = offset;
55
+ if (limit !== undefined)
56
+ qs.limit = limit;
57
+ const body = {};
58
+ if (query)
59
+ body.query = query;
60
+ if (statusList)
61
+ body.statusList = statusList;
62
+ if (accountCategoryKeys)
63
+ body.accountCategoryKeys = accountCategoryKeys;
64
+ if (projectKeys)
65
+ body.projectKeys = projectKeys;
66
+ const data = await client.request('POST', '/4/accounts/search', body, qs);
67
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
68
+ });
69
+ server.registerTool('tempo_create_account', {
50
70
  description: 'Create a new Tempo account.',
51
71
  annotations: { readOnlyHint: false },
52
72
  inputSchema: {
53
- type: 'object',
54
- properties: {
55
- key: { type: 'string', description: 'Unique account key' },
56
- name: { type: 'string', description: 'Account name' },
57
- status: { type: 'string', enum: ['OPEN', 'CLOSED', 'ARCHIVED'], description: 'Account status (default OPEN)' },
58
- leadAccountId: { type: 'string', description: 'Atlassian account id of the account lead' },
59
- categoryKey: { type: 'string', description: 'Account category key' },
60
- contactAccountId: { type: 'string', description: 'Atlassian account id of the contact person' },
61
- externalContactName: { type: 'string', description: 'Name of external contact' },
62
- monthlyBudget: { type: 'integer', description: 'Monthly budget in seconds' },
63
- },
64
- required: ['key', 'name'],
73
+ key: z.string().describe('Unique account key'),
74
+ name: z.string().describe('Account name'),
75
+ status: z.enum(['OPEN', 'CLOSED', 'ARCHIVED']).optional().describe('Account status (default OPEN)'),
76
+ leadAccountId: z.string().optional().describe('Atlassian account id of the account lead'),
77
+ categoryKey: z.string().optional().describe('Account category key'),
78
+ contactAccountId: z.string().optional().describe('Atlassian account id of the contact person'),
79
+ externalContactName: z.string().optional().describe('Name of external contact'),
80
+ monthlyBudget: z.number().int().optional().describe('Monthly budget in seconds'),
65
81
  },
66
- },
67
- {
68
- name: 'tempo_update_account',
82
+ }, async (args) => {
83
+ const body = buildAccountBody(args);
84
+ const data = await client.request('POST', '/4/accounts', body);
85
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
86
+ });
87
+ server.registerTool('tempo_update_account', {
69
88
  description: 'Update an existing Tempo account by its key.',
70
89
  annotations: { readOnlyHint: false },
71
90
  inputSchema: {
72
- type: 'object',
73
- properties: {
74
- key: { type: 'string', description: 'Account key to update' },
75
- name: { type: 'string', description: 'Account name' },
76
- status: { type: 'string', enum: ['OPEN', 'CLOSED', 'ARCHIVED'], description: 'Account status' },
77
- leadAccountId: { type: 'string', description: 'Atlassian account id of the account lead' },
78
- categoryKey: { type: 'string', description: 'Account category key' },
79
- contactAccountId: { type: 'string', description: 'Atlassian account id of the contact person' },
80
- externalContactName: { type: 'string', description: 'Name of external contact' },
81
- monthlyBudget: { type: 'integer', description: 'Monthly budget in seconds' },
82
- },
83
- required: ['key', 'name'],
91
+ key: z.string().describe('Account key to update'),
92
+ name: z.string().describe('Account name'),
93
+ status: z.enum(['OPEN', 'CLOSED', 'ARCHIVED']).optional().describe('Account status'),
94
+ leadAccountId: z.string().optional().describe('Atlassian account id of the account lead'),
95
+ categoryKey: z.string().optional().describe('Account category key'),
96
+ contactAccountId: z.string().optional().describe('Atlassian account id of the contact person'),
97
+ externalContactName: z.string().optional().describe('Name of external contact'),
98
+ monthlyBudget: z.number().int().optional().describe('Monthly budget in seconds'),
84
99
  },
85
- },
86
- {
87
- name: 'tempo_delete_account',
100
+ }, async ({ key, ...rest }) => {
101
+ const body = buildAccountBody({ key, ...rest });
102
+ const data = await client.request('PUT', `/4/accounts/${key}`, body);
103
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
104
+ });
105
+ server.registerTool('tempo_delete_account', {
88
106
  description: 'Delete a Tempo account by its key.',
89
107
  annotations: { readOnlyHint: false },
90
108
  inputSchema: {
91
- type: 'object',
92
- properties: {
93
- key: { type: 'string', description: 'Account key to delete' },
94
- },
95
- required: ['key'],
109
+ key: z.string().describe('Account key to delete'),
96
110
  },
97
- },
98
- {
99
- name: 'tempo_get_account_categories',
111
+ }, async ({ key }) => {
112
+ await client.request('DELETE', `/4/accounts/${key}`);
113
+ return { content: [{ type: 'text', text: `Account ${key} deleted successfully` }] };
114
+ });
115
+ server.registerTool('tempo_get_account_categories', {
100
116
  description: 'Retrieve all Tempo account categories.',
101
117
  annotations: { readOnlyHint: true },
102
118
  inputSchema: {
103
- type: 'object',
104
- properties: {
105
- offset: { type: 'integer', description: 'Pagination offset' },
106
- limit: { type: 'integer', description: 'Max results' },
107
- },
108
- required: [],
119
+ offset: z.number().int().optional().describe('Pagination offset'),
120
+ limit: z.number().int().optional().describe('Max results'),
109
121
  },
110
- },
111
- ];
112
- function buildAccountBody(args) {
113
- const body = { key: args.key, name: args.name };
114
- if (args.status !== undefined)
115
- body.status = args.status;
116
- if (args.leadAccountId !== undefined)
117
- body.leadAccountId = args.leadAccountId;
118
- if (args.categoryKey !== undefined)
119
- body.categoryKey = args.categoryKey;
120
- if (args.contactAccountId !== undefined)
121
- body.contactAccountId = args.contactAccountId;
122
- if (args.externalContactName !== undefined)
123
- body.externalContactName = args.externalContactName;
124
- if (args.monthlyBudget !== undefined)
125
- body.monthlyBudget = args.monthlyBudget;
126
- return body;
127
- }
128
- export async function handleTool(name, args, client) {
129
- switch (name) {
130
- case 'tempo_get_accounts': {
131
- const { offset, limit } = args;
132
- const data = await client.request('GET', '/4/accounts', undefined, { offset, limit });
133
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
134
- }
135
- case 'tempo_get_account': {
136
- const { key } = args;
137
- const data = await client.request('GET', `/4/accounts/${key}`);
138
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
139
- }
140
- case 'tempo_search_accounts': {
141
- const { query, statusList, accountCategoryKeys, projectKeys, offset, limit } = args;
142
- const qs = {};
143
- if (offset !== undefined)
144
- qs.offset = offset;
145
- if (limit !== undefined)
146
- qs.limit = limit;
147
- const body = {};
148
- if (query)
149
- body.query = query;
150
- if (statusList)
151
- body.statusList = statusList;
152
- if (accountCategoryKeys)
153
- body.accountCategoryKeys = accountCategoryKeys;
154
- if (projectKeys)
155
- body.projectKeys = projectKeys;
156
- const data = await client.request('POST', '/4/accounts/search', body, qs);
157
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
158
- }
159
- case 'tempo_create_account': {
160
- const body = buildAccountBody(args);
161
- const data = await client.request('POST', '/4/accounts', body);
162
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
163
- }
164
- case 'tempo_update_account': {
165
- const { key, ...rest } = args;
166
- const body = buildAccountBody({ key, ...rest });
167
- const data = await client.request('PUT', `/4/accounts/${key}`, body);
168
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
169
- }
170
- case 'tempo_delete_account': {
171
- const { key } = args;
172
- await client.request('DELETE', `/4/accounts/${key}`);
173
- return { content: [{ type: 'text', text: `Account ${key} deleted successfully` }] };
174
- }
175
- case 'tempo_get_account_categories': {
176
- const { offset, limit } = args;
177
- const data = await client.request('GET', '/4/account-categories', undefined, { offset, limit });
178
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
179
- }
180
- default:
181
- throw new Error(`Unknown tool: ${name}`);
182
- }
122
+ }, async ({ offset, limit }) => {
123
+ const data = await client.request('GET', '/4/account-categories', undefined, { offset, limit });
124
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
125
+ });
183
126
  }
@@ -1,104 +1,4 @@
1
- export const toolDefinitions = [
2
- {
3
- name: 'tempo_get_plans',
4
- description: 'Retrieve a list of Tempo plans (resource allocations) matching the given parameters. Requires from and to dates.',
5
- annotations: { readOnlyHint: true },
6
- inputSchema: {
7
- type: 'object',
8
- properties: {
9
- from: { type: 'string', description: 'Start date (YYYY-MM-DD) — required' },
10
- to: { type: 'string', description: 'End date (YYYY-MM-DD) — required' },
11
- accountIds: { type: 'array', items: { type: 'string' }, description: 'Filter by user account ids' },
12
- assigneeTypes: { type: 'array', items: { type: 'string', enum: ['USER', 'GENERIC'] }, description: 'Filter by assignee type' },
13
- genericResourceIds: { type: 'array', items: { type: 'integer' }, description: 'Filter by generic resource ids' },
14
- issueIds: { type: 'array', items: { type: 'integer' }, description: 'Filter by Jira issue ids' },
15
- projectIds: { type: 'array', items: { type: 'integer' }, description: 'Filter by Jira project ids' },
16
- planIds: { type: 'array', items: { type: 'integer' }, description: 'Filter by specific plan ids' },
17
- planItemTypes: { type: 'array', items: { type: 'string', enum: ['ISSUE', 'PROJECT'] }, description: 'Filter by plan item type' },
18
- plannedTimeBreakdown: { type: 'array', items: { type: 'string', enum: ['DAILY', 'PERIOD'] }, description: 'Time breakdown granularity' },
19
- updatedFrom: { type: 'string', description: 'Filter by update date' },
20
- offset: { type: 'integer', description: 'Pagination offset' },
21
- limit: { type: 'integer', description: 'Max results (max 5000)' },
22
- },
23
- required: ['from', 'to'],
24
- },
25
- },
26
- {
27
- name: 'tempo_get_plan',
28
- description: 'Retrieve a single Tempo plan (resource allocation) by id.',
29
- annotations: { readOnlyHint: true },
30
- inputSchema: {
31
- type: 'object',
32
- properties: {
33
- id: { type: 'integer', description: 'Plan id' },
34
- },
35
- required: ['id'],
36
- },
37
- },
38
- {
39
- name: 'tempo_create_plan',
40
- description: 'Create a new Tempo plan (resource allocation) for a user or generic resource against an issue or project.',
41
- annotations: { readOnlyHint: false },
42
- inputSchema: {
43
- type: 'object',
44
- properties: {
45
- assigneeId: { type: 'string', description: 'Atlassian account id (for USER) or generic resource id (for GENERIC)' },
46
- assigneeType: { type: 'string', enum: ['USER', 'GENERIC'], description: 'Type of assignee' },
47
- planItemId: { type: 'string', description: 'Id of the issue or project to plan against' },
48
- planItemType: { type: 'string', enum: ['ISSUE', 'PROJECT'], description: 'Type of plan item' },
49
- startDate: { type: 'string', description: 'Plan start date (YYYY-MM-DD)' },
50
- endDate: { type: 'string', description: 'Plan end date (YYYY-MM-DD)' },
51
- plannedSeconds: { type: 'integer', description: 'Total seconds planned (for TOTAL_SECONDS persistence type)' },
52
- plannedSecondsPerDay: { type: 'integer', description: 'Seconds planned per day (for SECONDS_PER_DAY persistence type)' },
53
- effortPersistenceType: { type: 'string', enum: ['SECONDS_PER_DAY', 'TOTAL_SECONDS'], description: 'How effort is distributed' },
54
- description: { type: 'string', description: 'Plan description' },
55
- startTime: { type: 'string', description: 'Start time (HH:mm)', pattern: '^([0-1]?[0-9]|2[0-3])(:[0-5][0-9])$' },
56
- includeNonWorkingDays: { type: 'boolean', description: 'Include non-working days in plan' },
57
- rule: { type: 'string', enum: ['NEVER', 'WEEKLY', 'BI_WEEKLY', 'MONTHLY'], description: 'Recurrence rule' },
58
- recurrenceEndDate: { type: 'string', description: 'End date for recurrence (YYYY-MM-DD)' },
59
- },
60
- required: ['assigneeId', 'assigneeType', 'planItemId', 'planItemType', 'startDate', 'endDate'],
61
- },
62
- },
63
- {
64
- name: 'tempo_update_plan',
65
- description: 'Update an existing Tempo plan (resource allocation) by id.',
66
- annotations: { readOnlyHint: false },
67
- inputSchema: {
68
- type: 'object',
69
- properties: {
70
- id: { type: 'integer', description: 'Plan id' },
71
- assigneeId: { type: 'string', description: 'Atlassian account id or generic resource id' },
72
- assigneeType: { type: 'string', enum: ['USER', 'GENERIC'], description: 'Type of assignee' },
73
- planItemId: { type: 'string', description: 'Id of the issue or project' },
74
- planItemType: { type: 'string', enum: ['ISSUE', 'PROJECT'], description: 'Type of plan item' },
75
- startDate: { type: 'string', description: 'Plan start date (YYYY-MM-DD)' },
76
- endDate: { type: 'string', description: 'Plan end date (YYYY-MM-DD)' },
77
- plannedSeconds: { type: 'integer', description: 'Total seconds planned' },
78
- plannedSecondsPerDay: { type: 'integer', description: 'Seconds planned per day' },
79
- effortPersistenceType: { type: 'string', enum: ['SECONDS_PER_DAY', 'TOTAL_SECONDS'], description: 'How effort is distributed' },
80
- description: { type: 'string', description: 'Plan description' },
81
- startTime: { type: 'string', description: 'Start time (HH:mm)' },
82
- includeNonWorkingDays: { type: 'boolean', description: 'Include non-working days in plan' },
83
- rule: { type: 'string', enum: ['NEVER', 'WEEKLY', 'BI_WEEKLY', 'MONTHLY'], description: 'Recurrence rule' },
84
- recurrenceEndDate: { type: 'string', description: 'End date for recurrence' },
85
- },
86
- required: ['id', 'assigneeId', 'assigneeType', 'planItemId', 'planItemType', 'startDate', 'endDate'],
87
- },
88
- },
89
- {
90
- name: 'tempo_delete_plan',
91
- description: 'Delete a Tempo plan (resource allocation) by id.',
92
- annotations: { readOnlyHint: false },
93
- inputSchema: {
94
- type: 'object',
95
- properties: {
96
- id: { type: 'integer', description: 'Plan id' },
97
- },
98
- required: ['id'],
99
- },
100
- },
101
- ];
1
+ import { z } from 'zod';
102
2
  function buildPlanBody(args) {
103
3
  const body = {
104
4
  assigneeId: args.assigneeId,
@@ -126,37 +26,84 @@ function buildPlanBody(args) {
126
26
  body.recurrenceEndDate = args.recurrenceEndDate;
127
27
  return body;
128
28
  }
129
- export async function handleTool(name, args, client) {
130
- switch (name) {
131
- case 'tempo_get_plans': {
132
- const { from, to, accountIds, assigneeTypes, genericResourceIds, issueIds, projectIds, planIds, planItemTypes, plannedTimeBreakdown, updatedFrom, offset, limit } = args;
133
- const data = await client.request('GET', '/4/plans', undefined, {
134
- from, to, accountIds, assigneeTypes, genericResourceIds, issueIds, projectIds, planIds, planItemTypes, plannedTimeBreakdown, updatedFrom, offset, limit,
135
- });
136
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
137
- }
138
- case 'tempo_get_plan': {
139
- const { id } = args;
140
- const data = await client.request('GET', `/4/plans/${id}`);
141
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
142
- }
143
- case 'tempo_create_plan': {
144
- const body = buildPlanBody(args);
145
- const data = await client.request('POST', '/4/plans', body);
146
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
147
- }
148
- case 'tempo_update_plan': {
149
- const { id, ...rest } = args;
150
- const body = buildPlanBody(rest);
151
- const data = await client.request('PUT', `/4/plans/${id}`, body);
152
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
153
- }
154
- case 'tempo_delete_plan': {
155
- const { id } = args;
156
- await client.request('DELETE', `/4/plans/${id}`);
157
- return { content: [{ type: 'text', text: `Plan ${id} deleted successfully` }] };
158
- }
159
- default:
160
- throw new Error(`Unknown tool: ${name}`);
161
- }
29
+ const planFields = {
30
+ assigneeId: z.string().describe('Atlassian account id (for USER) or generic resource id (for GENERIC)'),
31
+ assigneeType: z.enum(['USER', 'GENERIC']).describe('Type of assignee'),
32
+ planItemId: z.string().describe('Id of the issue or project to plan against'),
33
+ planItemType: z.enum(['ISSUE', 'PROJECT']).describe('Type of plan item'),
34
+ startDate: z.string().describe('Plan start date (YYYY-MM-DD)'),
35
+ endDate: z.string().describe('Plan end date (YYYY-MM-DD)'),
36
+ plannedSeconds: z.number().int().optional().describe('Total seconds planned (for TOTAL_SECONDS persistence type)'),
37
+ plannedSecondsPerDay: z.number().int().optional().describe('Seconds planned per day (for SECONDS_PER_DAY persistence type)'),
38
+ effortPersistenceType: z.enum(['SECONDS_PER_DAY', 'TOTAL_SECONDS']).optional().describe('How effort is distributed'),
39
+ description: z.string().optional().describe('Plan description'),
40
+ startTime: z.string().regex(/^([0-1]?[0-9]|2[0-3])(:[0-5][0-9])$/).optional().describe('Start time (HH:mm)'),
41
+ includeNonWorkingDays: z.boolean().optional().describe('Include non-working days in plan'),
42
+ rule: z.enum(['NEVER', 'WEEKLY', 'BI_WEEKLY', 'MONTHLY']).optional().describe('Recurrence rule'),
43
+ recurrenceEndDate: z.string().optional().describe('End date for recurrence (YYYY-MM-DD)'),
44
+ };
45
+ export function register(server, client) {
46
+ server.registerTool('tempo_get_plans', {
47
+ description: 'Retrieve a list of Tempo plans (resource allocations) matching the given parameters. Requires from and to dates.',
48
+ annotations: { readOnlyHint: true },
49
+ inputSchema: {
50
+ from: z.string().describe('Start date (YYYY-MM-DD) — required'),
51
+ to: z.string().describe('End date (YYYY-MM-DD) — required'),
52
+ accountIds: z.array(z.string()).optional().describe('Filter by user account ids'),
53
+ assigneeTypes: z.array(z.enum(['USER', 'GENERIC'])).optional().describe('Filter by assignee type'),
54
+ genericResourceIds: z.array(z.number().int()).optional().describe('Filter by generic resource ids'),
55
+ issueIds: z.array(z.number().int()).optional().describe('Filter by Jira issue ids'),
56
+ projectIds: z.array(z.number().int()).optional().describe('Filter by Jira project ids'),
57
+ planIds: z.array(z.number().int()).optional().describe('Filter by specific plan ids'),
58
+ planItemTypes: z.array(z.enum(['ISSUE', 'PROJECT'])).optional().describe('Filter by plan item type'),
59
+ plannedTimeBreakdown: z.array(z.enum(['DAILY', 'PERIOD'])).optional().describe('Time breakdown granularity'),
60
+ updatedFrom: z.string().optional().describe('Filter by update date'),
61
+ offset: z.number().int().optional().describe('Pagination offset'),
62
+ limit: z.number().int().optional().describe('Max results (max 5000)'),
63
+ },
64
+ }, async (args) => {
65
+ const data = await client.request('GET', '/4/plans', undefined, args);
66
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
67
+ });
68
+ server.registerTool('tempo_get_plan', {
69
+ description: 'Retrieve a single Tempo plan (resource allocation) by id.',
70
+ annotations: { readOnlyHint: true },
71
+ inputSchema: {
72
+ id: z.number().int().describe('Plan id'),
73
+ },
74
+ }, async ({ id }) => {
75
+ const data = await client.request('GET', `/4/plans/${id}`);
76
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
77
+ });
78
+ server.registerTool('tempo_create_plan', {
79
+ description: 'Create a new Tempo plan (resource allocation) for a user or generic resource against an issue or project.',
80
+ annotations: { readOnlyHint: false },
81
+ inputSchema: planFields,
82
+ }, async (args) => {
83
+ const body = buildPlanBody(args);
84
+ const data = await client.request('POST', '/4/plans', body);
85
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
86
+ });
87
+ server.registerTool('tempo_update_plan', {
88
+ description: 'Update an existing Tempo plan (resource allocation) by id.',
89
+ annotations: { readOnlyHint: false },
90
+ inputSchema: {
91
+ id: z.number().int().describe('Plan id'),
92
+ ...planFields,
93
+ },
94
+ }, async ({ id, ...rest }) => {
95
+ const body = buildPlanBody(rest);
96
+ const data = await client.request('PUT', `/4/plans/${id}`, body);
97
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
98
+ });
99
+ server.registerTool('tempo_delete_plan', {
100
+ description: 'Delete a Tempo plan (resource allocation) by id.',
101
+ annotations: { readOnlyHint: false },
102
+ inputSchema: {
103
+ id: z.number().int().describe('Plan id'),
104
+ },
105
+ }, async ({ id }) => {
106
+ await client.request('DELETE', `/4/plans/${id}`);
107
+ return { content: [{ type: 'text', text: `Plan ${id} deleted successfully` }] };
108
+ });
162
109
  }