tempo-api-mcp 1.0.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/dist/client.js +74 -0
- package/dist/index.js +54 -0
- package/dist/tools/accounts.js +183 -0
- package/dist/tools/plans.js +162 -0
- package/dist/tools/projects.js +203 -0
- package/dist/tools/teams.js +176 -0
- package/dist/tools/worklogs.js +290 -0
- package/package.json +34 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { config as loadDotenv } from 'dotenv';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
loadDotenv({ path: join(__dirname, '..', '.env'), override: false, quiet: true });
|
|
6
|
+
const BASE_URL = 'https://api.tempo.io';
|
|
7
|
+
export class TempoClient {
|
|
8
|
+
apiToken;
|
|
9
|
+
constructor() {
|
|
10
|
+
const token = process.env.TEMPO_API_TOKEN;
|
|
11
|
+
if (!token)
|
|
12
|
+
throw new Error('TEMPO_API_TOKEN environment variable is required');
|
|
13
|
+
this.apiToken = token;
|
|
14
|
+
}
|
|
15
|
+
async request(method, path, body, queryParams) {
|
|
16
|
+
return this.doRequest(method, path, body, queryParams, false);
|
|
17
|
+
}
|
|
18
|
+
async doRequest(method, path, body, queryParams, isRetry) {
|
|
19
|
+
const headers = {
|
|
20
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
Accept: 'application/json',
|
|
23
|
+
};
|
|
24
|
+
let url = `${BASE_URL}${path}`;
|
|
25
|
+
if (queryParams) {
|
|
26
|
+
const params = new URLSearchParams();
|
|
27
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
28
|
+
if (value === undefined || value === null)
|
|
29
|
+
continue;
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
for (const item of value) {
|
|
32
|
+
params.append(key, String(item));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
params.set(key, String(value));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const qs = params.toString();
|
|
40
|
+
if (qs)
|
|
41
|
+
url += `?${qs}`;
|
|
42
|
+
}
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
method,
|
|
45
|
+
headers,
|
|
46
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
47
|
+
});
|
|
48
|
+
if (response.status === 401) {
|
|
49
|
+
throw new Error('TEMPO_API_TOKEN is invalid or expired');
|
|
50
|
+
}
|
|
51
|
+
if (response.status === 429) {
|
|
52
|
+
if (!isRetry) {
|
|
53
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
54
|
+
return this.doRequest(method, path, body, queryParams, true);
|
|
55
|
+
}
|
|
56
|
+
throw new Error('Rate limited by Tempo API');
|
|
57
|
+
}
|
|
58
|
+
if (response.status === 204) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
let errorText;
|
|
63
|
+
try {
|
|
64
|
+
const errorBody = await response.json();
|
|
65
|
+
errorText = errorBody.message ?? JSON.stringify(errorBody);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
errorText = response.statusText;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Tempo API error: ${response.status} ${errorText} for ${method} ${path}`);
|
|
71
|
+
}
|
|
72
|
+
return response.json();
|
|
73
|
+
}
|
|
74
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
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';
|
|
11
|
+
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: '1.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
|
+
});
|
|
52
|
+
console.error('[tempo-api-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.');
|
|
53
|
+
const transport = new StdioServerTransport();
|
|
54
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
export const toolDefinitions = [
|
|
2
|
+
{
|
|
3
|
+
name: 'tempo_get_accounts',
|
|
4
|
+
description: 'Retrieve a list of all Tempo accounts (OPEN and CLOSED).',
|
|
5
|
+
annotations: { readOnlyHint: true },
|
|
6
|
+
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: [],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'tempo_get_account',
|
|
17
|
+
description: 'Retrieve a single Tempo account by its key.',
|
|
18
|
+
annotations: { readOnlyHint: true },
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
key: { type: 'string', description: 'Account key (e.g. ACCOUNT-123)' },
|
|
23
|
+
},
|
|
24
|
+
required: ['key'],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'tempo_search_accounts',
|
|
29
|
+
description: 'Search Tempo accounts with advanced filters (status, category, project).',
|
|
30
|
+
annotations: { readOnlyHint: true },
|
|
31
|
+
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: [],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'tempo_create_account',
|
|
50
|
+
description: 'Create a new Tempo account.',
|
|
51
|
+
annotations: { readOnlyHint: false },
|
|
52
|
+
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'],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'tempo_update_account',
|
|
69
|
+
description: 'Update an existing Tempo account by its key.',
|
|
70
|
+
annotations: { readOnlyHint: false },
|
|
71
|
+
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'],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'tempo_delete_account',
|
|
88
|
+
description: 'Delete a Tempo account by its key.',
|
|
89
|
+
annotations: { readOnlyHint: false },
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
key: { type: 'string', description: 'Account key to delete' },
|
|
94
|
+
},
|
|
95
|
+
required: ['key'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'tempo_get_account_categories',
|
|
100
|
+
description: 'Retrieve all Tempo account categories.',
|
|
101
|
+
annotations: { readOnlyHint: true },
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
106
|
+
limit: { type: 'integer', description: 'Max results' },
|
|
107
|
+
},
|
|
108
|
+
required: [],
|
|
109
|
+
},
|
|
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
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
];
|
|
102
|
+
function buildPlanBody(args) {
|
|
103
|
+
const body = {
|
|
104
|
+
assigneeId: args.assigneeId,
|
|
105
|
+
assigneeType: args.assigneeType,
|
|
106
|
+
planItemId: args.planItemId,
|
|
107
|
+
planItemType: args.planItemType,
|
|
108
|
+
startDate: args.startDate,
|
|
109
|
+
endDate: args.endDate,
|
|
110
|
+
};
|
|
111
|
+
if (args.plannedSeconds !== undefined)
|
|
112
|
+
body.plannedSeconds = args.plannedSeconds;
|
|
113
|
+
if (args.plannedSecondsPerDay !== undefined)
|
|
114
|
+
body.plannedSecondsPerDay = args.plannedSecondsPerDay;
|
|
115
|
+
if (args.effortPersistenceType !== undefined)
|
|
116
|
+
body.effortPersistenceType = args.effortPersistenceType;
|
|
117
|
+
if (args.description !== undefined)
|
|
118
|
+
body.description = args.description;
|
|
119
|
+
if (args.startTime !== undefined)
|
|
120
|
+
body.startTime = args.startTime;
|
|
121
|
+
if (args.includeNonWorkingDays !== undefined)
|
|
122
|
+
body.includeNonWorkingDays = args.includeNonWorkingDays;
|
|
123
|
+
if (args.rule !== undefined)
|
|
124
|
+
body.rule = args.rule;
|
|
125
|
+
if (args.recurrenceEndDate !== undefined)
|
|
126
|
+
body.recurrenceEndDate = args.recurrenceEndDate;
|
|
127
|
+
return body;
|
|
128
|
+
}
|
|
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
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
export const toolDefinitions = [
|
|
2
|
+
{
|
|
3
|
+
name: 'tempo_get_projects',
|
|
4
|
+
description: 'Retrieve a paginated list of all Tempo Financial Manager projects.',
|
|
5
|
+
annotations: { readOnlyHint: true },
|
|
6
|
+
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: [],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'tempo_get_project',
|
|
17
|
+
description: 'Retrieve a single Tempo Financial Manager project by id.',
|
|
18
|
+
annotations: { readOnlyHint: true },
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
id: { type: 'string', description: 'Project id' },
|
|
23
|
+
},
|
|
24
|
+
required: ['id'],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'tempo_get_timesheet_approval_status',
|
|
29
|
+
description: 'Retrieve the current timesheet approval status for a user in the given period.',
|
|
30
|
+
annotations: { readOnlyHint: true },
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
accountId: { type: 'string', description: 'Atlassian account id of the user' },
|
|
35
|
+
from: { type: 'string', description: 'Period start date (YYYY-MM-DD)' },
|
|
36
|
+
to: { type: 'string', description: 'Period end date (YYYY-MM-DD)' },
|
|
37
|
+
},
|
|
38
|
+
required: ['accountId'],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'tempo_get_timesheet_approvals_waiting',
|
|
43
|
+
description: 'Retrieve all timesheets that are currently waiting for approval.',
|
|
44
|
+
annotations: { readOnlyHint: true },
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
49
|
+
limit: { type: 'integer', description: 'Max results' },
|
|
50
|
+
},
|
|
51
|
+
required: [],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'tempo_search_timesheet_approval_logs',
|
|
56
|
+
description: 'Search timesheet approval audit logs.',
|
|
57
|
+
annotations: { readOnlyHint: true },
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
accountIds: { type: 'array', items: { type: 'string' }, description: 'Filter by Atlassian account ids' },
|
|
62
|
+
reviewerIds: { type: 'array', items: { type: 'string' }, description: 'Filter by reviewer account ids' },
|
|
63
|
+
from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
64
|
+
to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
65
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
66
|
+
limit: { type: 'integer', description: 'Max results' },
|
|
67
|
+
},
|
|
68
|
+
required: [],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'tempo_get_periods',
|
|
73
|
+
description: 'Retrieve Tempo period definitions (used for timesheet approval cycles).',
|
|
74
|
+
annotations: { readOnlyHint: true },
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
79
|
+
to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
80
|
+
},
|
|
81
|
+
required: [],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'tempo_get_user_schedule',
|
|
86
|
+
description: 'Retrieve the work schedule for a user, including planned working hours per day.',
|
|
87
|
+
annotations: { readOnlyHint: true },
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
accountId: { type: 'string', description: 'Atlassian account id of the user' },
|
|
92
|
+
from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
93
|
+
to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
94
|
+
},
|
|
95
|
+
required: ['accountId', 'from', 'to'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'tempo_get_global_configuration',
|
|
100
|
+
description: 'Retrieve the global Tempo configuration settings.',
|
|
101
|
+
annotations: { readOnlyHint: true },
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {},
|
|
105
|
+
required: [],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'tempo_get_work_attributes',
|
|
110
|
+
description: 'Retrieve all Tempo work attributes (custom fields on worklogs).',
|
|
111
|
+
annotations: { readOnlyHint: true },
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
116
|
+
limit: { type: 'integer', description: 'Max results' },
|
|
117
|
+
},
|
|
118
|
+
required: [],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'tempo_get_roles',
|
|
123
|
+
description: 'Retrieve all Tempo roles.',
|
|
124
|
+
annotations: { readOnlyHint: true },
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
129
|
+
limit: { type: 'integer', description: 'Max results' },
|
|
130
|
+
},
|
|
131
|
+
required: [],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
export async function handleTool(name, args, client) {
|
|
136
|
+
switch (name) {
|
|
137
|
+
case 'tempo_get_projects': {
|
|
138
|
+
const { offset, limit } = args;
|
|
139
|
+
const data = await client.request('GET', '/4/projects', undefined, { offset, limit });
|
|
140
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
141
|
+
}
|
|
142
|
+
case 'tempo_get_project': {
|
|
143
|
+
const { id } = args;
|
|
144
|
+
const data = await client.request('GET', `/4/projects/${id}`);
|
|
145
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
146
|
+
}
|
|
147
|
+
case 'tempo_get_timesheet_approval_status': {
|
|
148
|
+
const { accountId, from, to } = args;
|
|
149
|
+
const data = await client.request('GET', `/4/timesheet-approvals/user/${accountId}`, undefined, { from, to });
|
|
150
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
151
|
+
}
|
|
152
|
+
case 'tempo_get_timesheet_approvals_waiting': {
|
|
153
|
+
const { offset, limit } = args;
|
|
154
|
+
const data = await client.request('GET', '/4/timesheet-approvals/waiting', undefined, { offset, limit });
|
|
155
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
156
|
+
}
|
|
157
|
+
case 'tempo_search_timesheet_approval_logs': {
|
|
158
|
+
const { accountIds, reviewerIds, from, to, offset, limit } = args;
|
|
159
|
+
const qs = {};
|
|
160
|
+
if (offset !== undefined)
|
|
161
|
+
qs.offset = offset;
|
|
162
|
+
if (limit !== undefined)
|
|
163
|
+
qs.limit = limit;
|
|
164
|
+
const body = {};
|
|
165
|
+
if (accountIds)
|
|
166
|
+
body.accountIds = accountIds;
|
|
167
|
+
if (reviewerIds)
|
|
168
|
+
body.reviewerIds = reviewerIds;
|
|
169
|
+
if (from)
|
|
170
|
+
body.from = from;
|
|
171
|
+
if (to)
|
|
172
|
+
body.to = to;
|
|
173
|
+
const data = await client.request('POST', '/4/timesheet-approvals/logs/search', body, qs);
|
|
174
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
175
|
+
}
|
|
176
|
+
case 'tempo_get_periods': {
|
|
177
|
+
const { from, to } = args;
|
|
178
|
+
const data = await client.request('GET', '/4/periods', undefined, { from, to });
|
|
179
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
180
|
+
}
|
|
181
|
+
case 'tempo_get_user_schedule': {
|
|
182
|
+
const { accountId, from, to } = args;
|
|
183
|
+
const data = await client.request('GET', `/4/user-schedule`, undefined, { accountId, from, to });
|
|
184
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
185
|
+
}
|
|
186
|
+
case 'tempo_get_global_configuration': {
|
|
187
|
+
const data = await client.request('GET', '/4/globalconfiguration');
|
|
188
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
189
|
+
}
|
|
190
|
+
case 'tempo_get_work_attributes': {
|
|
191
|
+
const { offset, limit } = args;
|
|
192
|
+
const data = await client.request('GET', '/4/work-attributes', undefined, { offset, limit });
|
|
193
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
194
|
+
}
|
|
195
|
+
case 'tempo_get_roles': {
|
|
196
|
+
const { offset, limit } = args;
|
|
197
|
+
const data = await client.request('GET', '/4/roles', undefined, { offset, limit });
|
|
198
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
199
|
+
}
|
|
200
|
+
default:
|
|
201
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
export const toolDefinitions = [
|
|
2
|
+
{
|
|
3
|
+
name: 'tempo_get_teams',
|
|
4
|
+
description: 'Retrieve a list of Tempo teams. Can filter by name, member account ids, or specific team ids.',
|
|
5
|
+
annotations: { readOnlyHint: true },
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
name: { type: 'string', description: 'Filter by team name' },
|
|
10
|
+
teamIds: { type: 'array', items: { type: 'integer' }, description: 'Filter by specific team ids' },
|
|
11
|
+
teamMembers: { type: 'array', items: { type: 'string' }, description: 'Filter by member Atlassian account ids' },
|
|
12
|
+
includeMemberships: { type: 'boolean', description: 'Include team member memberships in response' },
|
|
13
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
14
|
+
limit: { type: 'integer', description: 'Max results (default 50)' },
|
|
15
|
+
},
|
|
16
|
+
required: [],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'tempo_get_team',
|
|
21
|
+
description: 'Retrieve a single Tempo team by id.',
|
|
22
|
+
annotations: { readOnlyHint: true },
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
id: { type: 'integer', description: 'Team id' },
|
|
27
|
+
},
|
|
28
|
+
required: ['id'],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'tempo_create_team',
|
|
33
|
+
description: 'Create a new Tempo team.',
|
|
34
|
+
annotations: { readOnlyHint: false },
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
name: { type: 'string', description: 'Team name' },
|
|
39
|
+
summary: { type: 'string', description: 'Short description of the team' },
|
|
40
|
+
leadAccountId: { type: 'string', description: 'Atlassian account id of the team lead' },
|
|
41
|
+
programId: { type: 'integer', description: 'Id of the program this team belongs to' },
|
|
42
|
+
},
|
|
43
|
+
required: ['name'],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'tempo_update_team',
|
|
48
|
+
description: 'Update an existing Tempo team by id.',
|
|
49
|
+
annotations: { readOnlyHint: false },
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
id: { type: 'integer', description: 'Team id' },
|
|
54
|
+
name: { type: 'string', description: 'Team name' },
|
|
55
|
+
summary: { type: 'string', description: 'Short description of the team' },
|
|
56
|
+
leadAccountId: { type: 'string', description: 'Atlassian account id of the team lead' },
|
|
57
|
+
programId: { type: 'integer', description: 'Id of the program this team belongs to' },
|
|
58
|
+
},
|
|
59
|
+
required: ['id', 'name'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'tempo_delete_team',
|
|
64
|
+
description: 'Delete a Tempo team by id.',
|
|
65
|
+
annotations: { readOnlyHint: false },
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
id: { type: 'integer', description: 'Team id' },
|
|
70
|
+
},
|
|
71
|
+
required: ['id'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'tempo_get_team_memberships',
|
|
76
|
+
description: 'Retrieve team memberships, optionally filtered by account id or team id.',
|
|
77
|
+
annotations: { readOnlyHint: true },
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
accountIds: { type: 'array', items: { type: 'string' }, description: 'Filter by Atlassian account ids' },
|
|
82
|
+
teamIds: { type: 'array', items: { type: 'integer' }, description: 'Filter by team ids' },
|
|
83
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
84
|
+
limit: { type: 'integer', description: 'Max results (default 50)' },
|
|
85
|
+
},
|
|
86
|
+
required: [],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'tempo_search_team_memberships',
|
|
91
|
+
description: 'Search Tempo team memberships with advanced filters via POST.',
|
|
92
|
+
annotations: { readOnlyHint: true },
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
teamIds: { type: 'array', items: { type: 'integer' }, description: 'Filter by team ids' },
|
|
97
|
+
accountIds: { type: 'array', items: { type: 'string' }, description: 'Filter by Atlassian account ids' },
|
|
98
|
+
from: { type: 'string', description: 'Membership active from date (YYYY-MM-DD)' },
|
|
99
|
+
to: { type: 'string', description: 'Membership active to date (YYYY-MM-DD)' },
|
|
100
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
101
|
+
limit: { type: 'integer', description: 'Max results' },
|
|
102
|
+
},
|
|
103
|
+
required: [],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
function buildTeamBody(args) {
|
|
108
|
+
const body = { name: args.name };
|
|
109
|
+
if (args.summary !== undefined)
|
|
110
|
+
body.summary = args.summary;
|
|
111
|
+
if (args.leadAccountId !== undefined)
|
|
112
|
+
body.leadAccountId = args.leadAccountId;
|
|
113
|
+
if (args.programId !== undefined)
|
|
114
|
+
body.programId = args.programId;
|
|
115
|
+
return body;
|
|
116
|
+
}
|
|
117
|
+
export async function handleTool(name, args, client) {
|
|
118
|
+
switch (name) {
|
|
119
|
+
case 'tempo_get_teams': {
|
|
120
|
+
const { name: teamName, teamIds, teamMembers, includeMemberships, offset, limit } = args;
|
|
121
|
+
const data = await client.request('GET', '/4/teams', undefined, {
|
|
122
|
+
name: teamName, teamIds, teamMembers, includeMemberships, offset, limit,
|
|
123
|
+
});
|
|
124
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
125
|
+
}
|
|
126
|
+
case 'tempo_get_team': {
|
|
127
|
+
const { id } = args;
|
|
128
|
+
const data = await client.request('GET', `/4/teams/${id}`);
|
|
129
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
130
|
+
}
|
|
131
|
+
case 'tempo_create_team': {
|
|
132
|
+
const body = buildTeamBody(args);
|
|
133
|
+
const data = await client.request('POST', '/4/teams', body);
|
|
134
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
135
|
+
}
|
|
136
|
+
case 'tempo_update_team': {
|
|
137
|
+
const { id, ...rest } = args;
|
|
138
|
+
const body = buildTeamBody(rest);
|
|
139
|
+
const data = await client.request('PUT', `/4/teams/${id}`, body);
|
|
140
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
141
|
+
}
|
|
142
|
+
case 'tempo_delete_team': {
|
|
143
|
+
const { id } = args;
|
|
144
|
+
await client.request('DELETE', `/4/teams/${id}`);
|
|
145
|
+
return { content: [{ type: 'text', text: `Team ${id} deleted successfully` }] };
|
|
146
|
+
}
|
|
147
|
+
case 'tempo_get_team_memberships': {
|
|
148
|
+
const { accountIds, teamIds, offset, limit } = args;
|
|
149
|
+
const data = await client.request('GET', '/4/team-memberships', undefined, {
|
|
150
|
+
accountIds, teamIds, offset, limit,
|
|
151
|
+
});
|
|
152
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
153
|
+
}
|
|
154
|
+
case 'tempo_search_team_memberships': {
|
|
155
|
+
const { teamIds, accountIds, from, to, offset, limit } = args;
|
|
156
|
+
const query = {};
|
|
157
|
+
if (offset !== undefined)
|
|
158
|
+
query.offset = offset;
|
|
159
|
+
if (limit !== undefined)
|
|
160
|
+
query.limit = limit;
|
|
161
|
+
const body = {};
|
|
162
|
+
if (teamIds)
|
|
163
|
+
body.teamIds = teamIds;
|
|
164
|
+
if (accountIds)
|
|
165
|
+
body.accountIds = accountIds;
|
|
166
|
+
if (from)
|
|
167
|
+
body.from = from;
|
|
168
|
+
if (to)
|
|
169
|
+
body.to = to;
|
|
170
|
+
const data = await client.request('POST', '/4/team-memberships/search', body, query);
|
|
171
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
172
|
+
}
|
|
173
|
+
default:
|
|
174
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
export const toolDefinitions = [
|
|
2
|
+
{
|
|
3
|
+
name: 'tempo_get_worklogs',
|
|
4
|
+
description: 'Retrieve a list of Tempo worklogs matching the given search parameters. Supports filtering by project, issue, date range, and more.',
|
|
5
|
+
annotations: { readOnlyHint: true },
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
projectId: { type: 'array', items: { type: 'integer' }, description: 'Filter by project ids' },
|
|
10
|
+
issueId: { type: 'array', items: { type: 'integer' }, description: 'Filter by issue ids' },
|
|
11
|
+
from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
12
|
+
to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
13
|
+
updatedFrom: { type: 'string', description: 'Filter by update date/time (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ssZ)' },
|
|
14
|
+
offset: { type: 'integer', description: 'Pagination offset (default 0)' },
|
|
15
|
+
limit: { type: 'integer', description: 'Max results (default 50)' },
|
|
16
|
+
orderBy: { type: 'string', enum: ['ID', 'START_DATE_TIME', 'UPDATED'], description: 'Sort order (descending)' },
|
|
17
|
+
},
|
|
18
|
+
required: [],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'tempo_get_worklog',
|
|
23
|
+
description: 'Retrieve a single Tempo worklog by its id.',
|
|
24
|
+
annotations: { readOnlyHint: true },
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
id: { type: 'string', description: 'Worklog id' },
|
|
29
|
+
},
|
|
30
|
+
required: ['id'],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'tempo_create_worklog',
|
|
35
|
+
description: 'Create a new Tempo worklog.',
|
|
36
|
+
annotations: { readOnlyHint: false },
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
authorAccountId: { type: 'string', description: 'Atlassian account id of the worklog author' },
|
|
41
|
+
issueId: { type: 'integer', description: 'Jira issue id to log time against' },
|
|
42
|
+
startDate: { type: 'string', description: 'Work date (YYYY-MM-DD)' },
|
|
43
|
+
timeSpentSeconds: { type: 'integer', description: 'Time spent in seconds (e.g. 3600 = 1 hour)' },
|
|
44
|
+
startTime: { type: 'string', description: 'Start time (HH:mm:ss)', pattern: '^([0-1]?[0-9]|2[0-3])(:[0-5][0-9])(:[0-5][0-9])$' },
|
|
45
|
+
description: { type: 'string', description: 'Description of work done' },
|
|
46
|
+
billableSeconds: { type: 'integer', description: 'Billable seconds (defaults to timeSpentSeconds)' },
|
|
47
|
+
remainingEstimateSeconds: { type: 'integer', description: 'Remaining estimate in seconds' },
|
|
48
|
+
},
|
|
49
|
+
required: ['authorAccountId', 'issueId', 'startDate', 'timeSpentSeconds'],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'tempo_update_worklog',
|
|
54
|
+
description: 'Update an existing Tempo worklog by id.',
|
|
55
|
+
annotations: { readOnlyHint: false },
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
id: { type: 'string', description: 'Worklog id' },
|
|
60
|
+
authorAccountId: { type: 'string', description: 'Atlassian account id of the worklog author' },
|
|
61
|
+
startDate: { type: 'string', description: 'Work date (YYYY-MM-DD)' },
|
|
62
|
+
timeSpentSeconds: { type: 'integer', description: 'Time spent in seconds' },
|
|
63
|
+
startTime: { type: 'string', description: 'Start time (HH:mm:ss)' },
|
|
64
|
+
description: { type: 'string', description: 'Description of work done' },
|
|
65
|
+
billableSeconds: { type: 'integer', description: 'Billable seconds' },
|
|
66
|
+
remainingEstimateSeconds: { type: 'integer', description: 'Remaining estimate in seconds' },
|
|
67
|
+
},
|
|
68
|
+
required: ['id', 'authorAccountId', 'startDate', 'timeSpentSeconds'],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'tempo_delete_worklog',
|
|
73
|
+
description: 'Delete a Tempo worklog by id.',
|
|
74
|
+
annotations: { readOnlyHint: false },
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
id: { type: 'string', description: 'Worklog id' },
|
|
79
|
+
bypassPeriodClosuresAndApprovals: { type: 'boolean', description: 'Bypass period closures/approvals (requires Tempo Admin + Override Mode)' },
|
|
80
|
+
},
|
|
81
|
+
required: ['id'],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'tempo_search_worklogs',
|
|
86
|
+
description: 'Search Tempo worklogs using a POST body with advanced filters (author ids, issue ids, project ids, date range).',
|
|
87
|
+
annotations: { readOnlyHint: true },
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
authorIds: { type: 'array', items: { type: 'string' }, description: 'Atlassian account ids of worklog authors' },
|
|
92
|
+
issueIds: { type: 'array', items: { type: 'integer' }, description: 'Jira issue ids' },
|
|
93
|
+
projectIds: { type: 'array', items: { type: 'integer' }, description: 'Jira project ids' },
|
|
94
|
+
teamIds: { type: 'array', items: { type: 'integer' }, description: 'Tempo team ids' },
|
|
95
|
+
accountIds: { type: 'array', items: { type: 'string' }, description: 'Tempo account keys' },
|
|
96
|
+
from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
97
|
+
to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
98
|
+
updatedFrom: { type: 'string', description: 'Filter by update date' },
|
|
99
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
100
|
+
limit: { type: 'integer', description: 'Max results (default 50)' },
|
|
101
|
+
},
|
|
102
|
+
required: [],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'tempo_get_worklogs_by_user',
|
|
107
|
+
description: 'Retrieve all Tempo worklogs for a specific user (Atlassian account id).',
|
|
108
|
+
annotations: { readOnlyHint: true },
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
accountId: { type: 'string', description: 'Atlassian account id of the user' },
|
|
113
|
+
from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
114
|
+
to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
115
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
116
|
+
limit: { type: 'integer', description: 'Max results (default 50)' },
|
|
117
|
+
},
|
|
118
|
+
required: ['accountId'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'tempo_get_worklogs_by_project',
|
|
123
|
+
description: 'Retrieve all Tempo worklogs for a specific Jira project.',
|
|
124
|
+
annotations: { readOnlyHint: true },
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
projectId: { type: 'integer', description: 'Jira project id' },
|
|
129
|
+
from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
130
|
+
to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
131
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
132
|
+
limit: { type: 'integer', description: 'Max results (default 50)' },
|
|
133
|
+
},
|
|
134
|
+
required: ['projectId'],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'tempo_get_worklogs_by_issue',
|
|
139
|
+
description: 'Retrieve all Tempo worklogs for a specific Jira issue.',
|
|
140
|
+
annotations: { readOnlyHint: true },
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
issueId: { type: 'integer', description: 'Jira issue id' },
|
|
145
|
+
from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
146
|
+
to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
147
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
148
|
+
limit: { type: 'integer', description: 'Max results (default 50)' },
|
|
149
|
+
},
|
|
150
|
+
required: ['issueId'],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'tempo_get_worklogs_by_team',
|
|
155
|
+
description: 'Retrieve all Tempo worklogs for a specific Tempo team.',
|
|
156
|
+
annotations: { readOnlyHint: true },
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
teamId: { type: 'integer', description: 'Tempo team id' },
|
|
161
|
+
from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
162
|
+
to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
163
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
164
|
+
limit: { type: 'integer', description: 'Max results (default 50)' },
|
|
165
|
+
},
|
|
166
|
+
required: ['teamId'],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'tempo_get_worklogs_by_account',
|
|
171
|
+
description: 'Retrieve all Tempo worklogs associated to a Tempo account key.',
|
|
172
|
+
annotations: { readOnlyHint: true },
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: {
|
|
176
|
+
accountKey: { type: 'string', description: 'Tempo account key (e.g. ACCOUNT-123)' },
|
|
177
|
+
from: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
178
|
+
to: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
179
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
180
|
+
limit: { type: 'integer', description: 'Max results (default 50)' },
|
|
181
|
+
},
|
|
182
|
+
required: ['accountKey'],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
export async function handleTool(name, args, client) {
|
|
187
|
+
switch (name) {
|
|
188
|
+
case 'tempo_get_worklogs': {
|
|
189
|
+
const { projectId, issueId, from, to, updatedFrom, offset, limit, orderBy } = args;
|
|
190
|
+
const data = await client.request('GET', '/4/worklogs', undefined, {
|
|
191
|
+
projectId, issueId, from, to, updatedFrom, offset, limit, orderBy,
|
|
192
|
+
});
|
|
193
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
194
|
+
}
|
|
195
|
+
case 'tempo_get_worklog': {
|
|
196
|
+
const { id } = args;
|
|
197
|
+
const data = await client.request('GET', `/4/worklogs/${id}`);
|
|
198
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
199
|
+
}
|
|
200
|
+
case 'tempo_create_worklog': {
|
|
201
|
+
const { authorAccountId, issueId, startDate, timeSpentSeconds, startTime, description, billableSeconds, remainingEstimateSeconds } = args;
|
|
202
|
+
const body = { authorAccountId, issueId, startDate, timeSpentSeconds };
|
|
203
|
+
if (startTime !== undefined)
|
|
204
|
+
body.startTime = startTime;
|
|
205
|
+
if (description !== undefined)
|
|
206
|
+
body.description = description;
|
|
207
|
+
if (billableSeconds !== undefined)
|
|
208
|
+
body.billableSeconds = billableSeconds;
|
|
209
|
+
if (remainingEstimateSeconds !== undefined)
|
|
210
|
+
body.remainingEstimateSeconds = remainingEstimateSeconds;
|
|
211
|
+
const data = await client.request('POST', '/4/worklogs', body);
|
|
212
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
213
|
+
}
|
|
214
|
+
case 'tempo_update_worklog': {
|
|
215
|
+
const { id, authorAccountId, startDate, timeSpentSeconds, startTime, description, billableSeconds, remainingEstimateSeconds } = args;
|
|
216
|
+
const body = { authorAccountId, startDate, timeSpentSeconds };
|
|
217
|
+
if (startTime !== undefined)
|
|
218
|
+
body.startTime = startTime;
|
|
219
|
+
if (description !== undefined)
|
|
220
|
+
body.description = description;
|
|
221
|
+
if (billableSeconds !== undefined)
|
|
222
|
+
body.billableSeconds = billableSeconds;
|
|
223
|
+
if (remainingEstimateSeconds !== undefined)
|
|
224
|
+
body.remainingEstimateSeconds = remainingEstimateSeconds;
|
|
225
|
+
const data = await client.request('PUT', `/4/worklogs/${id}`, body);
|
|
226
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
227
|
+
}
|
|
228
|
+
case 'tempo_delete_worklog': {
|
|
229
|
+
const { id, bypassPeriodClosuresAndApprovals } = args;
|
|
230
|
+
await client.request('DELETE', `/4/worklogs/${id}`, undefined, {
|
|
231
|
+
bypassPeriodClosuresAndApprovals,
|
|
232
|
+
});
|
|
233
|
+
return { content: [{ type: 'text', text: `Worklog ${id} deleted successfully` }] };
|
|
234
|
+
}
|
|
235
|
+
case 'tempo_search_worklogs': {
|
|
236
|
+
const { authorIds, issueIds, projectIds, teamIds, accountIds, from, to, updatedFrom, offset, limit } = args;
|
|
237
|
+
const query = {};
|
|
238
|
+
if (offset !== undefined)
|
|
239
|
+
query.offset = offset;
|
|
240
|
+
if (limit !== undefined)
|
|
241
|
+
query.limit = limit;
|
|
242
|
+
const body = {};
|
|
243
|
+
if (authorIds)
|
|
244
|
+
body.authorIds = authorIds;
|
|
245
|
+
if (issueIds)
|
|
246
|
+
body.issueIds = issueIds;
|
|
247
|
+
if (projectIds)
|
|
248
|
+
body.projectIds = projectIds;
|
|
249
|
+
if (teamIds)
|
|
250
|
+
body.teamIds = teamIds;
|
|
251
|
+
if (accountIds)
|
|
252
|
+
body.accountIds = accountIds;
|
|
253
|
+
if (from)
|
|
254
|
+
body.from = from;
|
|
255
|
+
if (to)
|
|
256
|
+
body.to = to;
|
|
257
|
+
if (updatedFrom)
|
|
258
|
+
body.updatedFrom = updatedFrom;
|
|
259
|
+
const data = await client.request('POST', '/4/worklogs/search', body, query);
|
|
260
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
261
|
+
}
|
|
262
|
+
case 'tempo_get_worklogs_by_user': {
|
|
263
|
+
const { accountId, from, to, offset, limit } = args;
|
|
264
|
+
const data = await client.request('GET', `/4/worklogs/user/${accountId}`, undefined, { from, to, offset, limit });
|
|
265
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
266
|
+
}
|
|
267
|
+
case 'tempo_get_worklogs_by_project': {
|
|
268
|
+
const { projectId, from, to, offset, limit } = args;
|
|
269
|
+
const data = await client.request('GET', `/4/worklogs/project/${projectId}`, undefined, { from, to, offset, limit });
|
|
270
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
271
|
+
}
|
|
272
|
+
case 'tempo_get_worklogs_by_issue': {
|
|
273
|
+
const { issueId, from, to, offset, limit } = args;
|
|
274
|
+
const data = await client.request('GET', `/4/worklogs/issue/${issueId}`, undefined, { from, to, offset, limit });
|
|
275
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
276
|
+
}
|
|
277
|
+
case 'tempo_get_worklogs_by_team': {
|
|
278
|
+
const { teamId, from, to, offset, limit } = args;
|
|
279
|
+
const data = await client.request('GET', `/4/worklogs/team/${teamId}`, undefined, { from, to, offset, limit });
|
|
280
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
281
|
+
}
|
|
282
|
+
case 'tempo_get_worklogs_by_account': {
|
|
283
|
+
const { accountKey, from, to, offset, limit } = args;
|
|
284
|
+
const data = await client.request('GET', `/4/worklogs/account/${accountKey}`, undefined, { from, to, offset, limit });
|
|
285
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
286
|
+
}
|
|
287
|
+
default:
|
|
288
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tempo-api-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Tempo API MCP server for Claude — developed and maintained by AI (Claude Sonnet 4.6)",
|
|
5
|
+
"author": "Claude Sonnet 4.6 (AI) <https://www.anthropic.com/claude>",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/chrischall/tempo-api-mcp"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"tempo-api-mcp": "dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "node dist/index.js",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"test:coverage": "vitest run --coverage"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
26
|
+
"dotenv": "^17.3.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^24.12.0",
|
|
30
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
31
|
+
"typescript": "^5.9.3",
|
|
32
|
+
"vitest": "^4.1.0"
|
|
33
|
+
}
|
|
34
|
+
}
|