@studiometa/productive-mcp 0.5.0 → 0.6.1
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/Dockerfile +7 -0
- package/README.md +51 -43
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/crypto.js +1 -1
- package/dist/crypto.js.map +1 -1
- package/dist/formatters.d.ts +20 -0
- package/dist/formatters.d.ts.map +1 -1
- package/dist/handlers/bookings.d.ts +6 -0
- package/dist/handlers/bookings.d.ts.map +1 -0
- package/dist/handlers/comments.d.ts +6 -0
- package/dist/handlers/comments.d.ts.map +1 -0
- package/dist/handlers/companies.d.ts +6 -0
- package/dist/handlers/companies.d.ts.map +1 -0
- package/dist/handlers/deals.d.ts +6 -0
- package/dist/handlers/deals.d.ts.map +1 -0
- package/dist/handlers/index.d.ts +15 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/people.d.ts +7 -0
- package/dist/handlers/people.d.ts.map +1 -0
- package/dist/handlers/projects.d.ts +6 -0
- package/dist/handlers/projects.d.ts.map +1 -0
- package/dist/handlers/services.d.ts +6 -0
- package/dist/handlers/services.d.ts.map +1 -0
- package/dist/handlers/tasks.d.ts +6 -0
- package/dist/handlers/tasks.d.ts.map +1 -0
- package/dist/handlers/time.d.ts +6 -0
- package/dist/handlers/time.d.ts.map +1 -0
- package/dist/handlers/timers.d.ts +6 -0
- package/dist/handlers/timers.d.ts.map +1 -0
- package/dist/handlers/types.d.ts +78 -0
- package/dist/handlers/types.d.ts.map +1 -0
- package/dist/handlers/utils.d.ts +17 -0
- package/dist/handlers/utils.d.ts.map +1 -0
- package/dist/handlers.d.ts +3 -10
- package/dist/handlers.d.ts.map +1 -1
- package/dist/handlers.js +2 -233
- package/dist/handlers.js.map +1 -1
- package/dist/http.js +3 -3
- package/dist/http.js.map +1 -1
- package/dist/index-CmTDkz-y.js +480 -0
- package/dist/index-CmTDkz-y.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/oauth.d.ts +1 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +111 -12
- package/dist/oauth.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/stdio.d.ts.map +1 -1
- package/dist/stdio.js +1 -1
- package/dist/stdio.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +34 -5
- package/dist/tools.js.map +1 -1
- package/dist/version-BBHuTm1A.js +5 -0
- package/dist/{version-eQNCcjOb.js.map → version-BBHuTm1A.js.map} +1 -1
- package/package.json +46 -46
- package/dist/version-eQNCcjOb.js +0 -5
package/dist/handlers.js
CHANGED
|
@@ -1,236 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const MCP_FORMAT_OPTIONS = {
|
|
3
|
-
includeRelationshipIds: false,
|
|
4
|
-
includeTimestamps: false,
|
|
5
|
-
stripHtml: true
|
|
6
|
-
};
|
|
7
|
-
function compactify(obj, fieldsToRemove) {
|
|
8
|
-
const result = { ...obj };
|
|
9
|
-
for (const field of fieldsToRemove) {
|
|
10
|
-
delete result[field];
|
|
11
|
-
}
|
|
12
|
-
return result;
|
|
13
|
-
}
|
|
14
|
-
function formatTimeEntry(entry, options) {
|
|
15
|
-
const result = formatTimeEntry$1(entry, MCP_FORMAT_OPTIONS);
|
|
16
|
-
if (options?.compact) {
|
|
17
|
-
return compactify(result, ["note", "billable_time", "approved"]);
|
|
18
|
-
}
|
|
19
|
-
return result;
|
|
20
|
-
}
|
|
21
|
-
function formatProject(project, options) {
|
|
22
|
-
const result = formatProject$1(project, MCP_FORMAT_OPTIONS);
|
|
23
|
-
if (options?.compact) {
|
|
24
|
-
return compactify(result, ["budget"]);
|
|
25
|
-
}
|
|
26
|
-
return result;
|
|
27
|
-
}
|
|
28
|
-
function formatTask(task, options) {
|
|
29
|
-
const result = formatTask$1(task, { ...MCP_FORMAT_OPTIONS, included: options?.included });
|
|
30
|
-
if (options?.compact) {
|
|
31
|
-
return compactify(result, [
|
|
32
|
-
"description",
|
|
33
|
-
"initial_estimate",
|
|
34
|
-
"worked_time",
|
|
35
|
-
"remaining_time",
|
|
36
|
-
"project",
|
|
37
|
-
// Keep project_name but remove nested object
|
|
38
|
-
"company"
|
|
39
|
-
// Keep company name inline if needed
|
|
40
|
-
]);
|
|
41
|
-
}
|
|
42
|
-
return result;
|
|
43
|
-
}
|
|
44
|
-
function formatPerson(person, options) {
|
|
45
|
-
const result = formatPerson$1(person, MCP_FORMAT_OPTIONS);
|
|
46
|
-
if (options?.compact) {
|
|
47
|
-
return compactify(result, ["title", "first_name", "last_name"]);
|
|
48
|
-
}
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
51
|
-
function formatService(service, options) {
|
|
52
|
-
const result = formatService$1(service, MCP_FORMAT_OPTIONS);
|
|
53
|
-
if (options?.compact) {
|
|
54
|
-
return compactify(result, ["budgeted_time", "worked_time"]);
|
|
55
|
-
}
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
function formatListResponse(data, formatter, meta, options) {
|
|
59
|
-
const wrappedFormatter = (item, _cliOptions) => {
|
|
60
|
-
return formatter(item, options);
|
|
61
|
-
};
|
|
62
|
-
const result = formatListResponse$1(data, wrappedFormatter, meta, {
|
|
63
|
-
...MCP_FORMAT_OPTIONS,
|
|
64
|
-
included: options?.included
|
|
65
|
-
});
|
|
66
|
-
return result;
|
|
67
|
-
}
|
|
68
|
-
const DEFAULT_PER_PAGE = 20;
|
|
69
|
-
function jsonResult(data) {
|
|
70
|
-
return {
|
|
71
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
function errorResult(message) {
|
|
75
|
-
return {
|
|
76
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
77
|
-
isError: true
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
function toStringFilter(filter) {
|
|
81
|
-
if (!filter) return void 0;
|
|
82
|
-
const result = {};
|
|
83
|
-
for (const [key, value] of Object.entries(filter)) {
|
|
84
|
-
if (value !== void 0 && value !== null) {
|
|
85
|
-
result[key] = String(value);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
89
|
-
}
|
|
90
|
-
async function executeToolWithCredentials(name, args, credentials) {
|
|
91
|
-
const api = new ProductiveApi({
|
|
92
|
-
token: credentials.apiToken,
|
|
93
|
-
"org-id": credentials.organizationId,
|
|
94
|
-
"user-id": credentials.userId
|
|
95
|
-
});
|
|
96
|
-
if (name === "productive") {
|
|
97
|
-
const {
|
|
98
|
-
resource,
|
|
99
|
-
action,
|
|
100
|
-
id,
|
|
101
|
-
filter,
|
|
102
|
-
page,
|
|
103
|
-
per_page,
|
|
104
|
-
compact = true,
|
|
105
|
-
person_id,
|
|
106
|
-
service_id,
|
|
107
|
-
task_id,
|
|
108
|
-
time,
|
|
109
|
-
date,
|
|
110
|
-
note
|
|
111
|
-
} = args;
|
|
112
|
-
const formatOptions = { compact };
|
|
113
|
-
const stringFilter = toStringFilter(filter);
|
|
114
|
-
const perPage = per_page ?? DEFAULT_PER_PAGE;
|
|
115
|
-
try {
|
|
116
|
-
if (resource === "projects") {
|
|
117
|
-
if (action === "get") {
|
|
118
|
-
if (!id) return errorResult("id is required for get action");
|
|
119
|
-
const result = await api.getProject(id);
|
|
120
|
-
return jsonResult(formatProject(result.data, formatOptions));
|
|
121
|
-
}
|
|
122
|
-
if (action === "list") {
|
|
123
|
-
const result = await api.getProjects({ filter: stringFilter, page, perPage });
|
|
124
|
-
return jsonResult(
|
|
125
|
-
formatListResponse(result.data, formatProject, result.meta, formatOptions)
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
return errorResult(`Invalid action "${action}" for projects. Use: list, get`);
|
|
129
|
-
}
|
|
130
|
-
if (resource === "time") {
|
|
131
|
-
if (action === "get") {
|
|
132
|
-
if (!id) return errorResult("id is required for get action");
|
|
133
|
-
const result = await api.getTimeEntry(id);
|
|
134
|
-
return jsonResult(formatTimeEntry(result.data, formatOptions));
|
|
135
|
-
}
|
|
136
|
-
if (action === "create") {
|
|
137
|
-
if (!person_id || !service_id || !time || !date) {
|
|
138
|
-
return errorResult("person_id, service_id, time, and date are required for create");
|
|
139
|
-
}
|
|
140
|
-
const result = await api.createTimeEntry({
|
|
141
|
-
person_id,
|
|
142
|
-
service_id,
|
|
143
|
-
time,
|
|
144
|
-
date,
|
|
145
|
-
note,
|
|
146
|
-
task_id
|
|
147
|
-
});
|
|
148
|
-
return jsonResult({ success: true, ...formatTimeEntry(result.data, formatOptions) });
|
|
149
|
-
}
|
|
150
|
-
if (action === "update") {
|
|
151
|
-
if (!id) return errorResult("id is required for update action");
|
|
152
|
-
const updateData = {};
|
|
153
|
-
if (time !== void 0) updateData.time = time;
|
|
154
|
-
if (date !== void 0) updateData.date = date;
|
|
155
|
-
if (note !== void 0) updateData.note = note;
|
|
156
|
-
const result = await api.updateTimeEntry(id, updateData);
|
|
157
|
-
return jsonResult({ success: true, ...formatTimeEntry(result.data, formatOptions) });
|
|
158
|
-
}
|
|
159
|
-
if (action === "delete") {
|
|
160
|
-
if (!id) return errorResult("id is required for delete action");
|
|
161
|
-
await api.deleteTimeEntry(id);
|
|
162
|
-
return jsonResult({ success: true, message: "Time entry deleted" });
|
|
163
|
-
}
|
|
164
|
-
if (action === "list") {
|
|
165
|
-
const result = await api.getTimeEntries({ filter: stringFilter, page, perPage });
|
|
166
|
-
return jsonResult(
|
|
167
|
-
formatListResponse(result.data, formatTimeEntry, result.meta, formatOptions)
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
return errorResult(`Invalid action "${action}" for time. Use: list, get, create, update, delete`);
|
|
171
|
-
}
|
|
172
|
-
if (resource === "tasks") {
|
|
173
|
-
const include = ["project", "project.company"];
|
|
174
|
-
if (action === "get") {
|
|
175
|
-
if (!id) return errorResult("id is required for get action");
|
|
176
|
-
const result = await api.getTask(id, { include });
|
|
177
|
-
return jsonResult(
|
|
178
|
-
formatTask(result.data, { ...formatOptions, included: result.included })
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
if (action === "list") {
|
|
182
|
-
const result = await api.getTasks({ filter: stringFilter, page, perPage, include });
|
|
183
|
-
return jsonResult(
|
|
184
|
-
formatListResponse(result.data, formatTask, result.meta, {
|
|
185
|
-
...formatOptions,
|
|
186
|
-
included: result.included
|
|
187
|
-
})
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
return errorResult(`Invalid action "${action}" for tasks. Use: list, get`);
|
|
191
|
-
}
|
|
192
|
-
if (resource === "services") {
|
|
193
|
-
if (action === "list") {
|
|
194
|
-
const result = await api.getServices({ filter: stringFilter, page, perPage });
|
|
195
|
-
return jsonResult(
|
|
196
|
-
formatListResponse(result.data, formatService, result.meta, formatOptions)
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
return errorResult(`Invalid action "${action}" for services. Use: list`);
|
|
200
|
-
}
|
|
201
|
-
if (resource === "people") {
|
|
202
|
-
if (action === "get") {
|
|
203
|
-
if (!id) return errorResult("id is required for get action");
|
|
204
|
-
const result = await api.getPerson(id);
|
|
205
|
-
return jsonResult(formatPerson(result.data, formatOptions));
|
|
206
|
-
}
|
|
207
|
-
if (action === "me") {
|
|
208
|
-
if (credentials.userId) {
|
|
209
|
-
const result = await api.getPerson(credentials.userId);
|
|
210
|
-
return jsonResult(formatPerson(result.data, formatOptions));
|
|
211
|
-
}
|
|
212
|
-
return jsonResult({
|
|
213
|
-
message: "User ID not configured. Set userId in credentials to use this action.",
|
|
214
|
-
organizationId: credentials.organizationId
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
if (action === "list") {
|
|
218
|
-
const result = await api.getPeople({ filter: stringFilter, page, perPage });
|
|
219
|
-
return jsonResult(
|
|
220
|
-
formatListResponse(result.data, formatPerson, result.meta, formatOptions)
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
return errorResult(`Invalid action "${action}" for people. Use: list, get, me`);
|
|
224
|
-
}
|
|
225
|
-
return errorResult(`Unknown resource: ${resource}`);
|
|
226
|
-
} catch (error) {
|
|
227
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
228
|
-
return errorResult(message);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return errorResult(`Unknown tool: ${name}`);
|
|
232
|
-
}
|
|
1
|
+
import { e } from "./index-CmTDkz-y.js";
|
|
233
2
|
export {
|
|
234
|
-
executeToolWithCredentials
|
|
3
|
+
e as executeToolWithCredentials
|
|
235
4
|
};
|
|
236
5
|
//# sourceMappingURL=handlers.js.map
|
package/dist/handlers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.js","sources":["../src/formatters.ts","../src/handlers.ts"],"sourcesContent":["/**\n * Response formatters for agent-friendly output\n *\n * This module re-exports formatters from @studiometa/productive-cli\n * with MCP-specific defaults (no relationship IDs, no timestamps).\n *\n * Supports compact mode to reduce token usage by omitting verbose fields\n * like descriptions and notes from list responses.\n */\n\nimport {\n formatTimeEntry as cliFormatTimeEntry,\n formatProject as cliFormatProject,\n formatTask as cliFormatTask,\n formatPerson as cliFormatPerson,\n formatService as cliFormatService,\n formatListResponse as cliFormatListResponse,\n type JsonApiResource,\n type JsonApiMeta,\n type FormatOptions,\n type FormattedPagination,\n} from '@studiometa/productive-cli';\n\n// Re-export types\nexport type { JsonApiResource, JsonApiMeta, FormatOptions, FormattedPagination };\n\n/**\n * MCP-specific format options\n * - No relationship IDs (cleaner output for agents)\n * - No timestamps (reduce noise)\n * - HTML stripping enabled\n */\nconst MCP_FORMAT_OPTIONS: FormatOptions = {\n includeRelationshipIds: false,\n includeTimestamps: false,\n stripHtml: true,\n};\n\n/**\n * Extended format options for MCP with compact mode\n */\nexport interface McpFormatOptions {\n compact?: boolean;\n included?: JsonApiResource[];\n}\n\n/**\n * Remove verbose fields from an object for compact output\n */\nfunction compactify<T extends Record<string, unknown>>(\n obj: T,\n fieldsToRemove: string[]\n): T {\n const result = { ...obj };\n for (const field of fieldsToRemove) {\n delete result[field];\n }\n return result;\n}\n\n/**\n * Format time entry for agent consumption\n */\nexport function formatTimeEntry(\n entry: JsonApiResource,\n options?: McpFormatOptions\n): Record<string, unknown> {\n const result = cliFormatTimeEntry(entry, MCP_FORMAT_OPTIONS);\n if (options?.compact) {\n return compactify(result, ['note', 'billable_time', 'approved']);\n }\n return result;\n}\n\n/**\n * Format project for agent consumption\n */\nexport function formatProject(\n project: JsonApiResource,\n options?: McpFormatOptions\n): Record<string, unknown> {\n const result = cliFormatProject(project, MCP_FORMAT_OPTIONS);\n if (options?.compact) {\n return compactify(result, ['budget']);\n }\n return result;\n}\n\n/**\n * Format task for agent consumption\n * Tasks use included resources to resolve project/company names\n */\nexport function formatTask(\n task: JsonApiResource,\n options?: McpFormatOptions\n): Record<string, unknown> {\n const result = cliFormatTask(task, { ...MCP_FORMAT_OPTIONS, included: options?.included });\n if (options?.compact) {\n return compactify(result, [\n 'description',\n 'initial_estimate',\n 'worked_time',\n 'remaining_time',\n 'project', // Keep project_name but remove nested object\n 'company', // Keep company name inline if needed\n ]);\n }\n return result;\n}\n\n/**\n * Format person for agent consumption\n */\nexport function formatPerson(\n person: JsonApiResource,\n options?: McpFormatOptions\n): Record<string, unknown> {\n const result = cliFormatPerson(person, MCP_FORMAT_OPTIONS);\n if (options?.compact) {\n return compactify(result, ['title', 'first_name', 'last_name']); // Keep 'name' which combines them\n }\n return result;\n}\n\n/**\n * Format service for agent consumption\n */\nexport function formatService(\n service: JsonApiResource,\n options?: McpFormatOptions\n): Record<string, unknown> {\n const result = cliFormatService(service, MCP_FORMAT_OPTIONS);\n if (options?.compact) {\n return compactify(result, ['budgeted_time', 'worked_time']);\n }\n return result;\n}\n\n/**\n * Format list response with pagination\n *\n * @param data - Array of JSON:API resources\n * @param formatter - Formatter function (item, options?) => T\n * @param meta - Pagination metadata\n * @param options - MCP format options (compact, included)\n */\nexport function formatListResponse<T>(\n data: JsonApiResource[],\n formatter: (item: JsonApiResource, options?: McpFormatOptions) => T,\n meta?: JsonApiMeta,\n options?: McpFormatOptions\n): { data: T[]; meta?: FormattedPagination } {\n // Create a wrapper that passes MCP options to the formatter\n const wrappedFormatter = (item: JsonApiResource, _cliOptions?: FormatOptions) => {\n return formatter(item, options);\n };\n\n const result = cliFormatListResponse(data, wrappedFormatter, meta, {\n ...MCP_FORMAT_OPTIONS,\n included: options?.included,\n });\n\n return result as { data: T[]; meta?: FormattedPagination };\n}\n","/**\n * Tool execution handlers for Productive MCP server\n * These are shared between stdio and HTTP transports\n *\n * Single consolidated tool for minimal token overhead:\n * - productive: resource + action based API\n */\n\nimport { ProductiveApi } from '@studiometa/productive-cli';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { ProductiveCredentials } from './auth.js';\nimport {\n formatTimeEntry,\n formatTask,\n formatProject,\n formatPerson,\n formatService,\n formatListResponse,\n type McpFormatOptions,\n} from './formatters.js';\n\nexport type ToolResult = CallToolResult;\n\n/** Default page size for MCP (smaller than CLI to reduce token usage) */\nconst DEFAULT_PER_PAGE = 20;\n\n/**\n * Helper to create a successful JSON response\n */\nfunction jsonResult(data: unknown): ToolResult {\n return {\n content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],\n };\n}\n\n/**\n * Helper to create an error response\n */\nfunction errorResult(message: string): ToolResult {\n return {\n content: [{ type: 'text', text: `Error: ${message}` }],\n isError: true,\n };\n}\n\n/**\n * Convert unknown filter to string filter for API\n */\nfunction toStringFilter(filter?: Record<string, unknown>): Record<string, string> | undefined {\n if (!filter) return undefined;\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(filter)) {\n if (value !== undefined && value !== null) {\n result[key] = String(value);\n }\n }\n return Object.keys(result).length > 0 ? result : undefined;\n}\n\n/**\n * Args interface for the consolidated tool\n */\ninterface ProductiveArgs {\n resource: 'projects' | 'time' | 'tasks' | 'services' | 'people';\n action: 'list' | 'get' | 'create' | 'update' | 'delete' | 'me';\n id?: string;\n filter?: Record<string, unknown>;\n page?: number;\n per_page?: number;\n compact?: boolean;\n // Time entry fields\n person_id?: string;\n service_id?: string;\n task_id?: string;\n time?: number;\n date?: string;\n note?: string;\n}\n\n/**\n * Execute a tool with the given credentials and arguments\n */\nexport async function executeToolWithCredentials(\n name: string,\n args: Record<string, unknown>,\n credentials: ProductiveCredentials\n): Promise<ToolResult> {\n // Initialize API client with provided credentials\n const api = new ProductiveApi({\n token: credentials.apiToken,\n 'org-id': credentials.organizationId,\n 'user-id': credentials.userId,\n } as Record<string, string>);\n\n // Handle the single consolidated tool\n if (name === 'productive') {\n const {\n resource,\n action,\n id,\n filter,\n page,\n per_page,\n compact = true,\n person_id,\n service_id,\n task_id,\n time,\n date,\n note,\n } = args as unknown as ProductiveArgs;\n\n const formatOptions: McpFormatOptions = { compact };\n const stringFilter = toStringFilter(filter);\n const perPage = per_page ?? DEFAULT_PER_PAGE;\n\n try {\n // ========================================================================\n // Projects\n // ========================================================================\n if (resource === 'projects') {\n if (action === 'get') {\n if (!id) return errorResult('id is required for get action');\n const result = await api.getProject(id);\n return jsonResult(formatProject(result.data, formatOptions));\n }\n if (action === 'list') {\n const result = await api.getProjects({ filter: stringFilter, page, perPage });\n return jsonResult(\n formatListResponse(result.data, formatProject, result.meta, formatOptions)\n );\n }\n return errorResult(`Invalid action \"${action}\" for projects. Use: list, get`);\n }\n\n // ========================================================================\n // Time Entries\n // ========================================================================\n if (resource === 'time') {\n if (action === 'get') {\n if (!id) return errorResult('id is required for get action');\n const result = await api.getTimeEntry(id);\n return jsonResult(formatTimeEntry(result.data, formatOptions));\n }\n\n if (action === 'create') {\n if (!person_id || !service_id || !time || !date) {\n return errorResult('person_id, service_id, time, and date are required for create');\n }\n const result = await api.createTimeEntry({\n person_id,\n service_id,\n time,\n date,\n note,\n task_id,\n });\n return jsonResult({ success: true, ...formatTimeEntry(result.data, formatOptions) });\n }\n\n if (action === 'update') {\n if (!id) return errorResult('id is required for update action');\n const updateData: Parameters<typeof api.updateTimeEntry>[1] = {};\n if (time !== undefined) updateData.time = time;\n if (date !== undefined) updateData.date = date;\n if (note !== undefined) updateData.note = note;\n const result = await api.updateTimeEntry(id, updateData);\n return jsonResult({ success: true, ...formatTimeEntry(result.data, formatOptions) });\n }\n\n if (action === 'delete') {\n if (!id) return errorResult('id is required for delete action');\n await api.deleteTimeEntry(id);\n return jsonResult({ success: true, message: 'Time entry deleted' });\n }\n\n if (action === 'list') {\n const result = await api.getTimeEntries({ filter: stringFilter, page, perPage });\n return jsonResult(\n formatListResponse(result.data, formatTimeEntry, result.meta, formatOptions)\n );\n }\n\n return errorResult(`Invalid action \"${action}\" for time. Use: list, get, create, update, delete`);\n }\n\n // ========================================================================\n // Tasks\n // ========================================================================\n if (resource === 'tasks') {\n const include = ['project', 'project.company'];\n\n if (action === 'get') {\n if (!id) return errorResult('id is required for get action');\n const result = await api.getTask(id, { include });\n return jsonResult(\n formatTask(result.data, { ...formatOptions, included: result.included })\n );\n }\n\n if (action === 'list') {\n const result = await api.getTasks({ filter: stringFilter, page, perPage, include });\n return jsonResult(\n formatListResponse(result.data, formatTask, result.meta, {\n ...formatOptions,\n included: result.included,\n })\n );\n }\n\n return errorResult(`Invalid action \"${action}\" for tasks. Use: list, get`);\n }\n\n // ========================================================================\n // Services\n // ========================================================================\n if (resource === 'services') {\n if (action === 'list') {\n const result = await api.getServices({ filter: stringFilter, page, perPage });\n return jsonResult(\n formatListResponse(result.data, formatService, result.meta, formatOptions)\n );\n }\n\n return errorResult(`Invalid action \"${action}\" for services. Use: list`);\n }\n\n // ========================================================================\n // People\n // ========================================================================\n if (resource === 'people') {\n if (action === 'get') {\n if (!id) return errorResult('id is required for get action');\n const result = await api.getPerson(id);\n return jsonResult(formatPerson(result.data, formatOptions));\n }\n\n if (action === 'me') {\n if (credentials.userId) {\n const result = await api.getPerson(credentials.userId);\n return jsonResult(formatPerson(result.data, formatOptions));\n }\n return jsonResult({\n message: 'User ID not configured. Set userId in credentials to use this action.',\n organizationId: credentials.organizationId,\n });\n }\n\n if (action === 'list') {\n const result = await api.getPeople({ filter: stringFilter, page, perPage });\n return jsonResult(\n formatListResponse(result.data, formatPerson, result.meta, formatOptions)\n );\n }\n\n return errorResult(`Invalid action \"${action}\" for people. Use: list, get, me`);\n }\n\n return errorResult(`Unknown resource: ${resource}`);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return errorResult(message);\n }\n }\n\n return errorResult(`Unknown tool: ${name}`);\n}\n"],"names":["cliFormatTimeEntry","cliFormatProject","cliFormatTask","cliFormatPerson","cliFormatService","cliFormatListResponse"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"handlers.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
package/dist/http.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createApp, createRouter, defineEventHandler, getHeader, setResponseHeader, readBody } from "h3";
|
|
2
|
-
import { TOOLS } from "./tools.js";
|
|
3
|
-
import { executeToolWithCredentials } from "./handlers.js";
|
|
4
2
|
import { parseAuthHeader } from "./auth.js";
|
|
5
|
-
import {
|
|
3
|
+
import { e as executeToolWithCredentials } from "./index-CmTDkz-y.js";
|
|
6
4
|
import { oauthMetadataHandler, registerHandler, authorizeGetHandler, authorizePostHandler, tokenHandler } from "./oauth.js";
|
|
5
|
+
import { TOOLS } from "./tools.js";
|
|
6
|
+
import { V as VERSION } from "./version-BBHuTm1A.js";
|
|
7
7
|
function jsonRpcError(code, message, id = null) {
|
|
8
8
|
return {
|
|
9
9
|
jsonrpc: "2.0",
|
package/dist/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","sources":["../src/http.ts"],"sourcesContent":["/**\n * HTTP transport handlers for Productive MCP Server\n *\n * This module contains the app/router creation logic for the HTTP transport.\n * The actual server startup is in server.ts.\n */\n\nimport {\n createApp,\n createRouter,\n defineEventHandler,\n readBody,\n getHeader,\n setResponseHeader,\n type App,\n} from 'h3';\n\nimport {
|
|
1
|
+
{"version":3,"file":"http.js","sources":["../src/http.ts"],"sourcesContent":["/**\n * HTTP transport handlers for Productive MCP Server\n *\n * This module contains the app/router creation logic for the HTTP transport.\n * The actual server startup is in server.ts.\n */\n\nimport {\n createApp,\n createRouter,\n defineEventHandler,\n readBody,\n getHeader,\n setResponseHeader,\n type App,\n} from 'h3';\n\nimport { parseAuthHeader } from './auth.js';\nimport { executeToolWithCredentials } from './handlers.js';\nimport {\n oauthMetadataHandler,\n registerHandler,\n authorizeGetHandler,\n authorizePostHandler,\n tokenHandler,\n} from './oauth.js';\nimport { TOOLS } from './tools.js';\nimport { VERSION } from './version.js';\n\n/**\n * JSON-RPC error response\n */\nexport function jsonRpcError(code: number, message: string, id: string | number | null = null) {\n return {\n jsonrpc: '2.0',\n error: { code, message },\n id,\n };\n}\n\n/**\n * JSON-RPC success response\n */\nexport function jsonRpcSuccess(result: unknown, id: string | number | null = null) {\n return {\n jsonrpc: '2.0',\n result,\n id,\n };\n}\n\n/**\n * Handle the initialize JSON-RPC method\n */\nexport function handleInitialize() {\n return {\n protocolVersion: '2024-11-05',\n serverInfo: {\n name: 'productive-mcp',\n version: VERSION,\n },\n capabilities: {\n tools: {},\n },\n };\n}\n\n/**\n * Handle the tools/list JSON-RPC method\n */\nexport function handleToolsList() {\n return { tools: TOOLS };\n}\n\n/**\n * Create the h3 application with all routes\n */\nexport function createHttpApp(): App {\n const app = createApp();\n const router = createRouter();\n\n // OAuth 2.0 endpoints for Claude Desktop integration (MCP auth spec)\n router.get('/.well-known/oauth-authorization-server', oauthMetadataHandler);\n router.post('/register', registerHandler); // Dynamic Client Registration (RFC 7591)\n router.get('/authorize', authorizeGetHandler);\n router.post('/authorize', authorizePostHandler);\n router.post('/token', tokenHandler);\n\n // Health check endpoint\n router.get(\n '/',\n defineEventHandler(() => {\n return { status: 'ok', service: 'productive-mcp', version: VERSION };\n }),\n );\n\n router.get(\n '/health',\n defineEventHandler(() => {\n return { status: 'ok' };\n }),\n );\n\n // MCP endpoint - handles JSON-RPC over HTTP\n router.post(\n '/mcp',\n defineEventHandler(async (event) => {\n // Parse authorization header\n const authHeader = getHeader(event, 'authorization');\n const credentials = parseAuthHeader(authHeader);\n\n if (!credentials) {\n setResponseHeader(event, 'Content-Type', 'application/json');\n event.node.res.statusCode = 401;\n return jsonRpcError(\n -32001,\n 'Authentication required. Provide Bearer token with base64(organizationId:apiToken:userId)',\n );\n }\n\n setResponseHeader(event, 'Content-Type', 'application/json');\n\n // Parse JSON-RPC request\n let body: { method?: string; params?: unknown; id?: string | number };\n try {\n body = await readBody(event);\n } catch {\n event.node.res.statusCode = 400;\n return jsonRpcError(-32700, 'Parse error: Invalid JSON');\n }\n\n if (!body || typeof body !== 'object') {\n event.node.res.statusCode = 400;\n return jsonRpcError(-32700, 'Parse error: Invalid JSON');\n }\n\n const { method, params, id } = body;\n\n try {\n if (method === 'initialize') {\n return jsonRpcSuccess(handleInitialize(), id ?? null);\n }\n\n if (method === 'tools/list') {\n return jsonRpcSuccess(handleToolsList(), id ?? null);\n }\n\n if (method === 'tools/call') {\n const { name, arguments: args } = params as {\n name: string;\n arguments?: Record<string, unknown>;\n };\n const result = await executeToolWithCredentials(name, args || {}, credentials);\n return jsonRpcSuccess(result, id ?? null);\n }\n\n // Unknown method\n return jsonRpcError(-32601, `Method not found: ${method}`, id ?? null);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return jsonRpcError(-32603, `Internal error: ${message}`, id ?? null);\n }\n }),\n );\n\n // SSE endpoint for server-sent events (optional, for streaming responses)\n router.get(\n '/mcp/sse',\n defineEventHandler(async (event) => {\n const authHeader = getHeader(event, 'authorization');\n const credentials = parseAuthHeader(authHeader);\n\n if (!credentials) {\n event.node.res.statusCode = 401;\n return { error: 'Authentication required' };\n }\n\n // Set SSE headers\n setResponseHeader(event, 'Content-Type', 'text/event-stream');\n setResponseHeader(event, 'Cache-Control', 'no-cache');\n setResponseHeader(event, 'Connection', 'keep-alive');\n\n // Generate session ID and send it\n const sessionId = crypto.randomUUID();\n\n // Send initial session event\n event.node.res.write(`event: session\\ndata: ${JSON.stringify({ sessionId })}\\n\\n`);\n\n // Keep connection alive\n const keepAlive = setInterval(() => {\n event.node.res.write(': keepalive\\n\\n');\n }, 30000);\n\n // Clean up on close\n event.node.req.on('close', () => {\n clearInterval(keepAlive);\n });\n\n // Don't end the response - keep it open for SSE\n return new Promise(() => {});\n }),\n );\n\n app.use(router);\n return app;\n}\n"],"names":[],"mappings":"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"}
|