@studiometa/productive-mcp 0.10.9 → 0.10.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -7
- package/dist/api-reference/generated.d.ts +3 -0
- package/dist/api-reference/generated.d.ts.map +1 -0
- package/dist/api-reference/types.d.ts +31 -0
- package/dist/api-reference/types.d.ts.map +1 -0
- package/dist/auth.js +2 -0
- package/dist/auth.js.map +1 -1
- package/dist/crypto.js +2 -0
- package/dist/crypto.js.map +1 -1
- package/dist/handlers/activities.d.ts +1 -0
- package/dist/handlers/activities.d.ts.map +1 -1
- package/dist/handlers/api-read.d.ts +14 -0
- package/dist/handlers/api-read.d.ts.map +1 -0
- package/dist/handlers/api-utils.d.ts +27 -0
- package/dist/handlers/api-utils.d.ts.map +1 -0
- package/dist/handlers/api-write.d.ts +10 -0
- package/dist/handlers/api-write.d.ts.map +1 -0
- package/dist/handlers/attachments.d.ts +1 -0
- package/dist/handlers/attachments.d.ts.map +1 -1
- package/dist/handlers/bookings.d.ts +1 -0
- package/dist/handlers/bookings.d.ts.map +1 -1
- package/dist/handlers/comments.d.ts +1 -0
- package/dist/handlers/comments.d.ts.map +1 -1
- package/dist/handlers/companies.d.ts +1 -0
- package/dist/handlers/companies.d.ts.map +1 -1
- package/dist/handlers/custom-fields.d.ts +1 -0
- package/dist/handlers/custom-fields.d.ts.map +1 -1
- package/dist/handlers/deals.d.ts +1 -0
- package/dist/handlers/deals.d.ts.map +1 -1
- package/dist/handlers/discussions.d.ts +1 -0
- package/dist/handlers/discussions.d.ts.map +1 -1
- package/dist/handlers/help.d.ts.map +1 -1
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/pages.d.ts +1 -0
- package/dist/handlers/pages.d.ts.map +1 -1
- package/dist/handlers/projects.d.ts +1 -0
- package/dist/handlers/projects.d.ts.map +1 -1
- package/dist/handlers/schema.d.ts.map +1 -1
- package/dist/handlers/services.d.ts +1 -0
- package/dist/handlers/services.d.ts.map +1 -1
- package/dist/handlers/tasks.d.ts +1 -0
- package/dist/handlers/tasks.d.ts.map +1 -1
- package/dist/handlers/time.d.ts +1 -0
- package/dist/handlers/time.d.ts.map +1 -1
- package/dist/handlers/timers.d.ts +1 -0
- package/dist/handlers/timers.d.ts.map +1 -1
- package/dist/handlers/types.d.ts +1 -0
- package/dist/handlers/types.d.ts.map +1 -1
- package/dist/handlers-B9FASjNJ.js +41290 -0
- package/dist/handlers-B9FASjNJ.js.map +1 -0
- package/dist/handlers.js +1 -1
- package/dist/http-B3J8ZV4I.js +2534 -0
- package/dist/http-B3J8ZV4I.js.map +1 -0
- package/dist/http.d.ts +5 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +2 -170
- package/dist/index.js +4 -3
- package/dist/oauth.d.ts +1 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +8 -116
- package/dist/oauth.js.map +1 -1
- package/dist/schema.d.ts +32 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/server.js +4 -3
- package/dist/{stdio-Bi1Lvp8O.js → stdio-BpKd5pcS.js} +9 -3
- package/dist/{stdio-Bi1Lvp8O.js.map → stdio-BpKd5pcS.js.map} +1 -1
- package/dist/stdio.js +1 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +196 -120
- package/dist/tools.js.map +1 -1
- package/dist/{version-BFw4junA.js → version-Dm6m3p60.js} +13 -7
- package/dist/{version-BFw4junA.js.map → version-Dm6m3p60.js.map} +1 -1
- package/package.json +3 -3
- package/skills/SKILL.md +113 -1
- package/dist/handlers-t95fhdps.js +0 -4225
- package/dist/handlers-t95fhdps.js.map +0 -1
- package/dist/http.js.map +0 -1
package/dist/http.js
CHANGED
|
@@ -1,170 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
|
|
3
|
-
import { TOOLS } from "./tools.js";
|
|
4
|
-
import { parseAuthHeader } from "./auth.js";
|
|
5
|
-
import { authorizeGetHandler, authorizePostHandler, oauthMetadataHandler, registerHandler, tokenHandler } from "./oauth.js";
|
|
6
|
-
import { H3, defineHandler } from "h3";
|
|
7
|
-
/**
|
|
8
|
-
* HTTP transport handlers for Productive MCP Server
|
|
9
|
-
*
|
|
10
|
-
* This module contains the app/router creation logic for the HTTP transport.
|
|
11
|
-
* The actual server startup is in server.ts.
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* JSON-RPC error response
|
|
15
|
-
*/
|
|
16
|
-
function jsonRpcError(code, message, id = null) {
|
|
17
|
-
return {
|
|
18
|
-
jsonrpc: "2.0",
|
|
19
|
-
error: {
|
|
20
|
-
code,
|
|
21
|
-
message
|
|
22
|
-
},
|
|
23
|
-
id
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* JSON-RPC success response
|
|
28
|
-
*/
|
|
29
|
-
function jsonRpcSuccess(result, id = null) {
|
|
30
|
-
return {
|
|
31
|
-
jsonrpc: "2.0",
|
|
32
|
-
result,
|
|
33
|
-
id
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Handle the initialize JSON-RPC method
|
|
38
|
-
*/
|
|
39
|
-
function handleInitialize() {
|
|
40
|
-
return {
|
|
41
|
-
protocolVersion: "2024-11-05",
|
|
42
|
-
serverInfo: {
|
|
43
|
-
name: "productive-mcp",
|
|
44
|
-
version: VERSION
|
|
45
|
-
},
|
|
46
|
-
capabilities: {
|
|
47
|
-
tools: {},
|
|
48
|
-
resources: {}
|
|
49
|
-
},
|
|
50
|
-
instructions: INSTRUCTIONS
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Handle the tools/list JSON-RPC method
|
|
55
|
-
*/
|
|
56
|
-
function handleToolsList() {
|
|
57
|
-
return { tools: TOOLS };
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Get base URL from event headers
|
|
61
|
-
*/
|
|
62
|
-
function getBaseUrl(event) {
|
|
63
|
-
const host = event.req.headers.get("host") || "localhost:3000";
|
|
64
|
-
return `${event.req.headers.get("x-forwarded-proto") || "http"}://${host}`;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Create the H3 application with all routes
|
|
68
|
-
*/
|
|
69
|
-
function createHttpApp() {
|
|
70
|
-
const app = new H3();
|
|
71
|
-
app.get("/.well-known/oauth-authorization-server", oauthMetadataHandler);
|
|
72
|
-
app.post("/register", registerHandler);
|
|
73
|
-
app.get("/authorize", authorizeGetHandler);
|
|
74
|
-
app.post("/authorize", authorizePostHandler);
|
|
75
|
-
app.post("/token", tokenHandler);
|
|
76
|
-
app.get("/.well-known/oauth-protected-resource", defineHandler((event) => {
|
|
77
|
-
const baseUrl = getBaseUrl(event);
|
|
78
|
-
event.res.headers.set("Content-Type", "application/json");
|
|
79
|
-
event.res.headers.set("Cache-Control", "public, max-age=3600");
|
|
80
|
-
return {
|
|
81
|
-
resource: `${baseUrl}/mcp`,
|
|
82
|
-
authorization_servers: [baseUrl],
|
|
83
|
-
scopes_supported: ["productive"],
|
|
84
|
-
bearer_methods_supported: ["header"]
|
|
85
|
-
};
|
|
86
|
-
}));
|
|
87
|
-
app.get("/", defineHandler(() => {
|
|
88
|
-
return {
|
|
89
|
-
status: "ok",
|
|
90
|
-
service: "productive-mcp",
|
|
91
|
-
version: VERSION
|
|
92
|
-
};
|
|
93
|
-
}));
|
|
94
|
-
app.get("/health", defineHandler(() => {
|
|
95
|
-
return { status: "ok" };
|
|
96
|
-
}));
|
|
97
|
-
app.post("/mcp", defineHandler(async (event) => {
|
|
98
|
-
const credentials = parseAuthHeader(event.req.headers.get("authorization"));
|
|
99
|
-
if (!credentials) {
|
|
100
|
-
const baseUrl = getBaseUrl(event);
|
|
101
|
-
event.res.headers.set("Content-Type", "application/json");
|
|
102
|
-
event.res.headers.set("WWW-Authenticate", `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`);
|
|
103
|
-
event.res.status = 401;
|
|
104
|
-
return jsonRpcError(-32001, "Authentication required. Provide Bearer token with base64(organizationId:apiToken:userId)");
|
|
105
|
-
}
|
|
106
|
-
event.res.headers.set("Content-Type", "application/json");
|
|
107
|
-
let body;
|
|
108
|
-
try {
|
|
109
|
-
body = await event.req.json();
|
|
110
|
-
} catch {
|
|
111
|
-
event.res.status = 400;
|
|
112
|
-
return jsonRpcError(-32700, "Parse error: Invalid JSON");
|
|
113
|
-
}
|
|
114
|
-
if (!body || typeof body !== "object") {
|
|
115
|
-
event.res.status = 400;
|
|
116
|
-
return jsonRpcError(-32700, "Parse error: Invalid JSON");
|
|
117
|
-
}
|
|
118
|
-
const { method, params, id } = body;
|
|
119
|
-
try {
|
|
120
|
-
if (method === "initialize") return jsonRpcSuccess(handleInitialize(), id ?? null);
|
|
121
|
-
if (method === "tools/list") return jsonRpcSuccess(handleToolsList(), id ?? null);
|
|
122
|
-
if (method === "tools/call") {
|
|
123
|
-
const { name, arguments: args } = params;
|
|
124
|
-
return jsonRpcSuccess(await executeToolWithCredentials(name, args || {}, credentials), id ?? null);
|
|
125
|
-
}
|
|
126
|
-
if (method === "resources/list") return jsonRpcSuccess({ resources: listResources() }, id ?? null);
|
|
127
|
-
if (method === "resources/templates/list") return jsonRpcSuccess({ resourceTemplates: listResourceTemplates() }, id ?? null);
|
|
128
|
-
if (method === "resources/read") {
|
|
129
|
-
const { uri } = params ?? {};
|
|
130
|
-
if (!uri) return jsonRpcError(-32602, "Invalid params: uri is required", id ?? null);
|
|
131
|
-
return jsonRpcSuccess(await readResource(uri, credentials), id ?? null);
|
|
132
|
-
}
|
|
133
|
-
return jsonRpcError(-32601, `Method not found: ${method}`, id ?? null);
|
|
134
|
-
} catch (error) {
|
|
135
|
-
return jsonRpcError(-32603, `Internal error: ${error instanceof Error ? error.message : String(error)}`, id ?? null);
|
|
136
|
-
}
|
|
137
|
-
}));
|
|
138
|
-
app.get("/mcp/sse", defineHandler(async (event) => {
|
|
139
|
-
if (!parseAuthHeader(event.req.headers.get("authorization"))) {
|
|
140
|
-
const baseUrl = getBaseUrl(event);
|
|
141
|
-
event.res.headers.set("WWW-Authenticate", `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`);
|
|
142
|
-
event.res.status = 401;
|
|
143
|
-
return { error: "Authentication required" };
|
|
144
|
-
}
|
|
145
|
-
const nodeRuntime = event.runtime?.node;
|
|
146
|
-
const nodeRes = nodeRuntime?.res;
|
|
147
|
-
if (!nodeRes) {
|
|
148
|
-
event.res.status = 501;
|
|
149
|
-
return { error: "SSE requires Node.js runtime" };
|
|
150
|
-
}
|
|
151
|
-
nodeRes.writeHead(200, {
|
|
152
|
-
"Content-Type": "text/event-stream",
|
|
153
|
-
"Cache-Control": "no-cache",
|
|
154
|
-
Connection: "keep-alive"
|
|
155
|
-
});
|
|
156
|
-
const sessionId = crypto.randomUUID();
|
|
157
|
-
nodeRes.write(`event: session\ndata: ${JSON.stringify({ sessionId })}\n\n`);
|
|
158
|
-
const keepAlive = setInterval(() => {
|
|
159
|
-
nodeRes.write(": keepalive\n\n");
|
|
160
|
-
}, 3e4);
|
|
161
|
-
nodeRuntime?.req.on("close", () => {
|
|
162
|
-
clearInterval(keepAlive);
|
|
163
|
-
});
|
|
164
|
-
return new Promise(() => {});
|
|
165
|
-
}));
|
|
166
|
-
return app;
|
|
167
|
-
}
|
|
168
|
-
export { createHttpApp, handleInitialize, handleToolsList, jsonRpcError, jsonRpcSuccess };
|
|
169
|
-
|
|
170
|
-
//# sourceMappingURL=http.js.map
|
|
1
|
+
import { a as handleToolsList, i as handleInitialize, n as createHttpMcpServer, o as jsonRpcError, r as createHttpMcpTransport, s as jsonRpcSuccess, t as createHttpApp } from "./http-B3J8ZV4I.js";
|
|
2
|
+
export { createHttpApp, createHttpMcpServer, createHttpMcpTransport, handleInitialize, handleToolsList, jsonRpcError, jsonRpcSuccess };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as INSTRUCTIONS, i as readResource, n as listResourceTemplates, r as listResources, t as VERSION } from "./version-
|
|
3
|
-
import "./
|
|
4
|
-
import { a as handlePrompt, n as getAvailableTools, s as handleToolCall, t as getAvailablePrompts } from "./stdio-Bi1Lvp8O.js";
|
|
2
|
+
import { a as INSTRUCTIONS, i as readResource, n as listResourceTemplates, r as listResources, t as VERSION } from "./version-Dm6m3p60.js";
|
|
3
|
+
import { a as handlePrompt, n as getAvailableTools, s as handleToolCall, t as getAvailablePrompts } from "./stdio-BpKd5pcS.js";
|
|
5
4
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
6
|
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
7
|
import { getConfig } from "@studiometa/productive-api";
|
|
8
|
+
//#region src/index.ts
|
|
9
9
|
/**
|
|
10
10
|
* Productive MCP Server - Stdio Transport
|
|
11
11
|
*
|
|
@@ -94,6 +94,7 @@ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith
|
|
|
94
94
|
console.error("Fatal error:", error);
|
|
95
95
|
process.exit(1);
|
|
96
96
|
});
|
|
97
|
+
//#endregion
|
|
97
98
|
export { createStdioServer, startStdioServer };
|
|
98
99
|
|
|
99
100
|
//# sourceMappingURL=index.js.map
|
package/dist/oauth.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export declare const authorizeGetHandler: import("h3").EventHandlerWithFetch<imp
|
|
|
68
68
|
* Authorization endpoint - process login
|
|
69
69
|
* POST /authorize
|
|
70
70
|
*/
|
|
71
|
-
export declare const authorizePostHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<string>>;
|
|
71
|
+
export declare const authorizePostHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<string | import("h3").HTTPResponse>>;
|
|
72
72
|
/**
|
|
73
73
|
* Token endpoint - exchange code for access token
|
|
74
74
|
* POST /token
|
package/dist/oauth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;EAyB/B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;GAoC1B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,0GA+C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;EAyB/B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;GAoC1B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,0GA+C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,mHA0D/B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;GA2FvB,CAAC"}
|
package/dist/oauth.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createAuthToken } from "./auth.js";
|
|
|
2
2
|
import { createAuthCode, decodeAuthCode } from "./crypto.js";
|
|
3
3
|
import { defineHandler, getQuery, redirect } from "h3";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
+
//#region src/oauth.ts
|
|
5
6
|
/**
|
|
6
7
|
* OAuth 2.0 endpoints for Claude Desktop integration
|
|
7
8
|
*
|
|
@@ -24,7 +25,7 @@ import { createHash } from "node:crypto";
|
|
|
24
25
|
*
|
|
25
26
|
* MCP clients MUST check this endpoint first for server capabilities.
|
|
26
27
|
*/
|
|
27
|
-
|
|
28
|
+
var oauthMetadataHandler = defineHandler((event) => {
|
|
28
29
|
const host = event.req.headers.get("host") || "localhost:3000";
|
|
29
30
|
const baseUrl = `${event.req.headers.get("x-forwarded-proto") || "http"}://${host}`;
|
|
30
31
|
event.res.headers.set("Content-Type", "application/json");
|
|
@@ -50,7 +51,7 @@ const oauthMetadataHandler = defineHandler((event) => {
|
|
|
50
51
|
* Since we use stateless tokens, we accept any registration and return
|
|
51
52
|
* a generated client_id.
|
|
52
53
|
*/
|
|
53
|
-
|
|
54
|
+
var registerHandler = defineHandler(async (event) => {
|
|
54
55
|
event.res.headers.set("Content-Type", "application/json");
|
|
55
56
|
let body;
|
|
56
57
|
try {
|
|
@@ -82,7 +83,7 @@ const registerHandler = defineHandler(async (event) => {
|
|
|
82
83
|
* Authorization endpoint - shows login form
|
|
83
84
|
* GET /authorize
|
|
84
85
|
*/
|
|
85
|
-
|
|
86
|
+
var authorizeGetHandler = defineHandler((event) => {
|
|
86
87
|
const query = getQuery(event);
|
|
87
88
|
const clientId = query.client_id;
|
|
88
89
|
const redirectUri = query.redirect_uri;
|
|
@@ -123,7 +124,7 @@ const authorizeGetHandler = defineHandler((event) => {
|
|
|
123
124
|
* Authorization endpoint - process login
|
|
124
125
|
* POST /authorize
|
|
125
126
|
*/
|
|
126
|
-
|
|
127
|
+
var authorizePostHandler = defineHandler(async (event) => {
|
|
127
128
|
const formData = await event.req.formData();
|
|
128
129
|
const { orgId, apiToken, userId, redirectUri, state, codeChallenge, codeChallengeMethod } = Object.fromEntries(formData.entries());
|
|
129
130
|
if (!redirectUri) {
|
|
@@ -163,8 +164,7 @@ const authorizePostHandler = defineHandler(async (event) => {
|
|
|
163
164
|
const redirectUrl = new URL(redirectUri);
|
|
164
165
|
redirectUrl.searchParams.set("code", code);
|
|
165
166
|
if (state) redirectUrl.searchParams.set("state", state);
|
|
166
|
-
|
|
167
|
-
return renderSuccessPage(redirectUrl.toString());
|
|
167
|
+
return redirect(redirectUrl.toString());
|
|
168
168
|
});
|
|
169
169
|
/**
|
|
170
170
|
* Token endpoint - exchange code for access token
|
|
@@ -174,7 +174,7 @@ const authorizePostHandler = defineHandler(async (event) => {
|
|
|
174
174
|
* - authorization_code grant (with PKCE validation)
|
|
175
175
|
* - refresh_token grant
|
|
176
176
|
*/
|
|
177
|
-
|
|
177
|
+
var tokenHandler = defineHandler(async (event) => {
|
|
178
178
|
event.res.headers.set("Content-Type", "application/json");
|
|
179
179
|
let body;
|
|
180
180
|
if ((event.req.headers.get("content-type") || "").includes("application/x-www-form-urlencoded")) {
|
|
@@ -457,115 +457,6 @@ function renderLoginForm(params) {
|
|
|
457
457
|
</html>`;
|
|
458
458
|
}
|
|
459
459
|
/**
|
|
460
|
-
* Render success page with auto-redirect
|
|
461
|
-
*/
|
|
462
|
-
function renderSuccessPage(redirectUrl) {
|
|
463
|
-
return `<!DOCTYPE html>
|
|
464
|
-
<html lang="en">
|
|
465
|
-
<head>
|
|
466
|
-
<meta charset="UTF-8">
|
|
467
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
468
|
-
<meta http-equiv="refresh" content="2;url=${escapeHtml(redirectUrl)}">
|
|
469
|
-
<title>Connected - Productive MCP</title>
|
|
470
|
-
<style>
|
|
471
|
-
* {
|
|
472
|
-
box-sizing: border-box;
|
|
473
|
-
margin: 0;
|
|
474
|
-
padding: 0;
|
|
475
|
-
}
|
|
476
|
-
body {
|
|
477
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
478
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
479
|
-
min-height: 100vh;
|
|
480
|
-
display: flex;
|
|
481
|
-
align-items: center;
|
|
482
|
-
justify-content: center;
|
|
483
|
-
padding: 20px;
|
|
484
|
-
}
|
|
485
|
-
.container {
|
|
486
|
-
background: white;
|
|
487
|
-
border-radius: 16px;
|
|
488
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
489
|
-
padding: 40px;
|
|
490
|
-
width: 100%;
|
|
491
|
-
max-width: 420px;
|
|
492
|
-
text-align: center;
|
|
493
|
-
}
|
|
494
|
-
.success-icon {
|
|
495
|
-
width: 64px;
|
|
496
|
-
height: 64px;
|
|
497
|
-
margin: 0 auto 24px;
|
|
498
|
-
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
499
|
-
border-radius: 50%;
|
|
500
|
-
display: flex;
|
|
501
|
-
align-items: center;
|
|
502
|
-
justify-content: center;
|
|
503
|
-
}
|
|
504
|
-
.success-icon svg {
|
|
505
|
-
width: 32px;
|
|
506
|
-
height: 32px;
|
|
507
|
-
stroke: white;
|
|
508
|
-
}
|
|
509
|
-
h1 {
|
|
510
|
-
color: #1a1a2e;
|
|
511
|
-
font-size: 24px;
|
|
512
|
-
margin-bottom: 8px;
|
|
513
|
-
}
|
|
514
|
-
.message {
|
|
515
|
-
color: #666;
|
|
516
|
-
font-size: 14px;
|
|
517
|
-
margin-bottom: 24px;
|
|
518
|
-
}
|
|
519
|
-
.spinner {
|
|
520
|
-
width: 24px;
|
|
521
|
-
height: 24px;
|
|
522
|
-
border: 3px solid #e5e7eb;
|
|
523
|
-
border-top-color: #667eea;
|
|
524
|
-
border-radius: 50%;
|
|
525
|
-
animation: spin 1s linear infinite;
|
|
526
|
-
margin: 0 auto 16px;
|
|
527
|
-
}
|
|
528
|
-
@keyframes spin {
|
|
529
|
-
to { transform: rotate(360deg); }
|
|
530
|
-
}
|
|
531
|
-
.redirect-text {
|
|
532
|
-
color: #9ca3af;
|
|
533
|
-
font-size: 13px;
|
|
534
|
-
margin-bottom: 16px;
|
|
535
|
-
}
|
|
536
|
-
.manual-link {
|
|
537
|
-
color: #667eea;
|
|
538
|
-
text-decoration: none;
|
|
539
|
-
font-size: 14px;
|
|
540
|
-
}
|
|
541
|
-
.manual-link:hover {
|
|
542
|
-
text-decoration: underline;
|
|
543
|
-
}
|
|
544
|
-
</style>
|
|
545
|
-
</head>
|
|
546
|
-
<body>
|
|
547
|
-
<div class="container">
|
|
548
|
-
<div class="success-icon">
|
|
549
|
-
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
550
|
-
<path d="M20 6L9 17L4 12" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
551
|
-
</svg>
|
|
552
|
-
</div>
|
|
553
|
-
<h1>Successfully Connected!</h1>
|
|
554
|
-
<p class="message">Your Productive.io credentials have been verified.</p>
|
|
555
|
-
<div class="spinner"></div>
|
|
556
|
-
<p class="redirect-text">Redirecting to Claude Desktop...</p>
|
|
557
|
-
<a href="${escapeHtml(redirectUrl)}" class="manual-link">Click here if not redirected automatically</a>
|
|
558
|
-
</div>
|
|
559
|
-
<script>
|
|
560
|
-
// Redirect after a short delay (backup for meta refresh)
|
|
561
|
-
setTimeout(function() {
|
|
562
|
-
window.location.href = ${JSON.stringify(redirectUrl)};
|
|
563
|
-
}, 2000);
|
|
564
|
-
<\/script>
|
|
565
|
-
</body>
|
|
566
|
-
</html>`;
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
460
|
* Render error page
|
|
570
461
|
*/
|
|
571
462
|
function renderErrorPage(message) {
|
|
@@ -616,6 +507,7 @@ function renderErrorPage(message) {
|
|
|
616
507
|
function escapeHtml(str) {
|
|
617
508
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
618
509
|
}
|
|
510
|
+
//#endregion
|
|
619
511
|
export { authorizeGetHandler, authorizePostHandler, oauthMetadataHandler, registerHandler, tokenHandler };
|
|
620
512
|
|
|
621
513
|
//# sourceMappingURL=oauth.js.map
|
package/dist/oauth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.js","names":[],"sources":["../src/oauth.ts"],"sourcesContent":["/**\n * OAuth 2.0 endpoints for Claude Desktop integration\n *\n * Implements OAuth 2.1 with PKCE as specified in the MCP authorization spec.\n * Uses stateless encrypted tokens - no server-side storage required.\n *\n * Spec: https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization\n *\n * Flow:\n * 1. Claude redirects user to /authorize with OAuth params (including PKCE)\n * 2. User enters Productive credentials in login form\n * 3. Server encrypts credentials + PKCE challenge into authorization code\n * 4. Redirects back to Claude with the code\n * 5. Claude exchanges code for access token via /token (with code_verifier)\n * 6. Server validates PKCE and returns access token\n */\n\nimport { defineHandler, getQuery, redirect, type H3Event } from 'h3';\nimport { createHash } from 'node:crypto';\n\nimport { createAuthToken } from './auth.js';\nimport { createAuthCode, decodeAuthCode } from './crypto.js';\n\n/**\n * OAuth metadata for discovery (RFC 8414)\n * GET /.well-known/oauth-authorization-server\n *\n * MCP clients MUST check this endpoint first for server capabilities.\n */\nexport const oauthMetadataHandler = defineHandler((event: H3Event) => {\n const host = event.req.headers.get('host') || 'localhost:3000';\n const protocol = event.req.headers.get('x-forwarded-proto') || 'http';\n const baseUrl = `${protocol}://${host}`;\n\n event.res.headers.set('Content-Type', 'application/json');\n event.res.headers.set('Cache-Control', 'public, max-age=3600');\n\n return {\n // Required fields per RFC 8414\n issuer: baseUrl,\n authorization_endpoint: `${baseUrl}/authorize`,\n token_endpoint: `${baseUrl}/token`,\n response_types_supported: ['code'],\n\n // OAuth 2.1 / MCP requirements\n grant_types_supported: ['authorization_code', 'refresh_token'],\n code_challenge_methods_supported: ['S256'],\n token_endpoint_auth_methods_supported: ['none'], // Public client\n\n // Optional but useful\n registration_endpoint: `${baseUrl}/register`,\n scopes_supported: ['productive'],\n service_documentation: 'https://github.com/studiometa/productive-tools',\n };\n});\n\n/**\n * Dynamic Client Registration endpoint (RFC 7591)\n * POST /register\n *\n * MCP servers SHOULD support DCR to allow clients to register automatically.\n * Since we use stateless tokens, we accept any registration and return\n * a generated client_id.\n */\nexport const registerHandler = defineHandler(async (event: H3Event) => {\n event.res.headers.set('Content-Type', 'application/json');\n\n let body: Record<string, unknown>;\n try {\n body = (await event.req.json()) as Record<string, unknown>;\n } catch {\n event.res.status = 400;\n return {\n error: 'invalid_request',\n error_description: 'Invalid JSON body',\n };\n }\n\n // Extract client metadata\n const clientName = (body.client_name as string) || 'MCP Client';\n const redirectUris = (body.redirect_uris as string[]) || [];\n\n // Generate a client_id based on the registration\n // Since we're stateless, we encode minimal info in the client_id\n const clientId = Buffer.from(\n JSON.stringify({\n name: clientName,\n ts: Date.now(),\n }),\n ).toString('base64url');\n\n event.res.status = 201;\n return {\n client_id: clientId,\n client_name: clientName,\n redirect_uris: redirectUris,\n token_endpoint_auth_method: 'none',\n grant_types: ['authorization_code', 'refresh_token'],\n response_types: ['code'],\n };\n});\n\n/**\n * Authorization endpoint - shows login form\n * GET /authorize\n */\nexport const authorizeGetHandler = defineHandler((event: H3Event) => {\n const query = getQuery(event);\n\n // Extract OAuth parameters\n const clientId = query.client_id as string;\n const redirectUri = query.redirect_uri as string;\n const state = query.state as string;\n const codeChallenge = query.code_challenge as string;\n const codeChallengeMethod = query.code_challenge_method as string;\n const scope = query.scope as string;\n\n // Validate required parameters per OAuth 2.1\n if (!redirectUri) {\n event.res.headers.set('Content-Type', 'text/html; charset=utf-8');\n event.res.status = 400;\n return renderErrorPage('Missing required parameter: redirect_uri');\n }\n\n // PKCE is REQUIRED for public clients per MCP spec\n if (!codeChallenge) {\n // Redirect back with error per OAuth spec\n const errorUrl = new URL(redirectUri);\n errorUrl.searchParams.set('error', 'invalid_request');\n errorUrl.searchParams.set('error_description', 'code_challenge is required');\n if (state) errorUrl.searchParams.set('state', state);\n return redirect(errorUrl.toString());\n }\n\n if (codeChallengeMethod && codeChallengeMethod !== 'S256') {\n const errorUrl = new URL(redirectUri);\n errorUrl.searchParams.set('error', 'invalid_request');\n errorUrl.searchParams.set('error_description', 'Only S256 code_challenge_method is supported');\n if (state) errorUrl.searchParams.set('state', state);\n return redirect(errorUrl.toString());\n }\n\n event.res.headers.set('Content-Type', 'text/html; charset=utf-8');\n\n // Render login form\n return renderLoginForm({\n clientId,\n redirectUri,\n state,\n codeChallenge,\n codeChallengeMethod: codeChallengeMethod || 'S256',\n scope,\n });\n});\n\n/**\n * Authorization endpoint - process login\n * POST /authorize\n */\nexport const authorizePostHandler = defineHandler(async (event: H3Event) => {\n const formData = await event.req.formData();\n const body = Object.fromEntries(formData.entries()) as Record<string, string>;\n\n const { orgId, apiToken, userId, redirectUri, state, codeChallenge, codeChallengeMethod } = body;\n\n // Validate redirect URI first (security requirement)\n if (!redirectUri) {\n event.res.headers.set('Content-Type', 'text/html; charset=utf-8');\n event.res.status = 400;\n return renderErrorPage('Missing redirect_uri parameter');\n }\n\n // Validate redirect URI format (must be HTTPS or localhost)\n try {\n const uri = new URL(redirectUri);\n const isLocalhost = uri.hostname === 'localhost' || uri.hostname === '127.0.0.1';\n const isHttps = uri.protocol === 'https:';\n if (!isLocalhost && !isHttps) {\n event.res.status = 400;\n return renderErrorPage('redirect_uri must be HTTPS or localhost');\n }\n } catch {\n event.res.status = 400;\n return renderErrorPage('Invalid redirect_uri format');\n }\n\n // Validate required credentials\n if (!orgId || !apiToken) {\n event.res.headers.set('Content-Type', 'text/html; charset=utf-8');\n return renderLoginForm({\n redirectUri,\n state,\n codeChallenge,\n codeChallengeMethod,\n error: 'Organization ID and API Token are required',\n });\n }\n\n // Create encrypted authorization code with PKCE challenge\n const code = createAuthCode({\n orgId,\n apiToken,\n userId: userId || undefined,\n codeChallenge,\n codeChallengeMethod: codeChallengeMethod || 'S256',\n });\n\n // Build redirect URL with authorization code\n const redirectUrl = new URL(redirectUri);\n redirectUrl.searchParams.set('code', code);\n if (state) {\n redirectUrl.searchParams.set('state', state);\n }\n\n // Show success page with auto-redirect\n event.res.headers.set('Content-Type', 'text/html; charset=utf-8');\n return renderSuccessPage(redirectUrl.toString());\n});\n\n/**\n * Token endpoint - exchange code for access token\n * POST /token\n *\n * Supports:\n * - authorization_code grant (with PKCE validation)\n * - refresh_token grant\n */\nexport const tokenHandler = defineHandler(async (event: H3Event) => {\n event.res.headers.set('Content-Type', 'application/json');\n\n let body: Record<string, string>;\n const contentType = event.req.headers.get('content-type') || '';\n\n if (contentType.includes('application/x-www-form-urlencoded')) {\n const rawText = await event.req.text();\n body = Object.fromEntries(new URLSearchParams(rawText));\n } else {\n body = (await event.req.json()) as Record<string, string>;\n }\n\n const { grant_type, code, code_verifier, refresh_token } = body;\n\n // Handle refresh token grant\n if (grant_type === 'refresh_token') {\n return handleRefreshToken(event, refresh_token);\n }\n\n // Validate authorization code grant\n if (grant_type !== 'authorization_code') {\n event.res.status = 400;\n return {\n error: 'unsupported_grant_type',\n error_description: 'Supported grant types: authorization_code, refresh_token',\n };\n }\n\n if (!code) {\n event.res.status = 400;\n return {\n error: 'invalid_request',\n error_description: 'Missing authorization code',\n };\n }\n\n if (!code_verifier) {\n event.res.status = 400;\n return {\n error: 'invalid_request',\n error_description: 'Missing code_verifier (PKCE required)',\n };\n }\n\n try {\n // Decode the authorization code\n const payload = decodeAuthCode(code);\n\n // Validate PKCE: SHA256(code_verifier) must equal code_challenge\n if (payload.codeChallenge) {\n const expectedChallenge = createS256Challenge(code_verifier);\n if (expectedChallenge !== payload.codeChallenge) {\n event.res.status = 400;\n return {\n error: 'invalid_grant',\n error_description: 'Invalid code_verifier',\n };\n }\n }\n\n // Create access token (base64 encoded credentials)\n const accessToken = createAuthToken({\n organizationId: payload.orgId,\n apiToken: payload.apiToken,\n userId: payload.userId,\n });\n\n // Create refresh token (encrypted credentials, longer expiry)\n const refreshToken = createAuthCode(\n {\n orgId: payload.orgId,\n apiToken: payload.apiToken,\n userId: payload.userId,\n },\n 86400 * 30, // 30 days\n );\n\n return {\n access_token: accessToken,\n token_type: 'Bearer',\n expires_in: 3600, // 1 hour (access tokens should be short-lived)\n refresh_token: refreshToken,\n };\n } catch (error) {\n event.res.status = 400;\n return {\n error: 'invalid_grant',\n error_description: error instanceof Error ? error.message : 'Invalid authorization code',\n };\n }\n});\n\n/**\n * Handle refresh token grant\n */\nfunction handleRefreshToken(event: H3Event, refreshToken: string | undefined) {\n if (!refreshToken) {\n event.res.status = 400;\n return {\n error: 'invalid_request',\n error_description: 'Missing refresh_token',\n };\n }\n\n try {\n // Decode refresh token (it's just an encrypted auth code with longer expiry)\n const payload = decodeAuthCode(refreshToken);\n\n // Create new access token\n const accessToken = createAuthToken({\n organizationId: payload.orgId,\n apiToken: payload.apiToken,\n userId: payload.userId,\n });\n\n // Create new refresh token (rotate for security)\n const newRefreshToken = createAuthCode(\n {\n orgId: payload.orgId,\n apiToken: payload.apiToken,\n userId: payload.userId,\n },\n 86400 * 30, // 30 days\n );\n\n return {\n access_token: accessToken,\n token_type: 'Bearer',\n expires_in: 3600,\n refresh_token: newRefreshToken,\n };\n } catch (error) {\n event.res.status = 400;\n return {\n error: 'invalid_grant',\n error_description: error instanceof Error ? error.message : 'Invalid refresh token',\n };\n }\n}\n\n/**\n * Create S256 PKCE challenge from verifier\n * SHA256(code_verifier) encoded as base64url\n */\nfunction createS256Challenge(codeVerifier: string): string {\n return createHash('sha256').update(codeVerifier).digest('base64url');\n}\n\n/**\n * Render the login form HTML\n */\nfunction renderLoginForm(params: {\n clientId?: string;\n redirectUri?: string;\n state?: string;\n codeChallenge?: string;\n codeChallengeMethod?: string;\n scope?: string;\n error?: string;\n}): string {\n const { redirectUri, state, codeChallenge, codeChallengeMethod, error } = params;\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Connect to Productive.io</title>\n <style>\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n padding: 40px;\n width: 100%;\n max-width: 420px;\n }\n .logo {\n text-align: center;\n margin-bottom: 24px;\n }\n .logo svg {\n width: 48px;\n height: 48px;\n }\n h1 {\n text-align: center;\n color: #1a1a2e;\n font-size: 24px;\n margin-bottom: 8px;\n }\n .subtitle {\n text-align: center;\n color: #666;\n font-size: 14px;\n margin-bottom: 32px;\n }\n .error {\n background: #fee2e2;\n border: 1px solid #fecaca;\n color: #dc2626;\n padding: 12px 16px;\n border-radius: 8px;\n margin-bottom: 24px;\n font-size: 14px;\n }\n .form-group {\n margin-bottom: 20px;\n }\n label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #374151;\n margin-bottom: 6px;\n }\n input {\n width: 100%;\n padding: 12px 16px;\n border: 1px solid #d1d5db;\n border-radius: 8px;\n font-size: 16px;\n transition: border-color 0.2s, box-shadow 0.2s;\n }\n input:focus {\n outline: none;\n border-color: #667eea;\n box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);\n }\n input::placeholder {\n color: #9ca3af;\n }\n .help-text {\n font-size: 12px;\n color: #6b7280;\n margin-top: 4px;\n }\n button {\n width: 100%;\n padding: 14px 24px;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: white;\n border: none;\n border-radius: 8px;\n font-size: 16px;\n font-weight: 600;\n cursor: pointer;\n transition: transform 0.2s, box-shadow 0.2s;\n }\n button:hover {\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);\n }\n button:active {\n transform: translateY(0);\n }\n .footer {\n text-align: center;\n margin-top: 24px;\n font-size: 12px;\n color: #9ca3af;\n }\n .footer a {\n color: #667eea;\n text-decoration: none;\n }\n .footer a:hover {\n text-decoration: underline;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"logo\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M12 2L2 7L12 12L22 7L12 2Z\" stroke=\"#667eea\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M2 17L12 22L22 17\" stroke=\"#764ba2\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M2 12L12 17L22 12\" stroke=\"#667eea\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n <h1>Connect to Productive.io</h1>\n <p class=\"subtitle\">Enter your Productive.io credentials to connect with Claude</p>\n \n ${error ? `<div class=\"error\">${escapeHtml(error)}</div>` : ''}\n \n <form method=\"POST\" action=\"/authorize\">\n <input type=\"hidden\" name=\"redirectUri\" value=\"${escapeHtml(redirectUri || '')}\">\n <input type=\"hidden\" name=\"state\" value=\"${escapeHtml(state || '')}\">\n <input type=\"hidden\" name=\"codeChallenge\" value=\"${escapeHtml(codeChallenge || '')}\">\n <input type=\"hidden\" name=\"codeChallengeMethod\" value=\"${escapeHtml(codeChallengeMethod || 'S256')}\">\n \n <div class=\"form-group\">\n <label for=\"orgId\">Organization ID *</label>\n <input type=\"text\" id=\"orgId\" name=\"orgId\" required placeholder=\"e.g., 12345\">\n <p class=\"help-text\">Found in Settings → API integrations</p>\n </div>\n \n <div class=\"form-group\">\n <label for=\"apiToken\">API Token *</label>\n <input type=\"password\" id=\"apiToken\" name=\"apiToken\" required placeholder=\"pk_...\">\n <p class=\"help-text\">Generate at Settings → API integrations → Generate new token</p>\n </div>\n \n <div class=\"form-group\">\n <label for=\"userId\">User ID (optional)</label>\n <input type=\"text\" id=\"userId\" name=\"userId\" placeholder=\"e.g., 67890\">\n <p class=\"help-text\">Required for creating time entries. Found in your profile URL.</p>\n </div>\n \n <button type=\"submit\">Connect to Productive</button>\n </form>\n \n <p class=\"footer\">\n Your credentials are encrypted and sent directly to Claude.<br>\n <a href=\"https://developer.productive.io\" target=\"_blank\">Productive.io API Documentation</a>\n </p>\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Render success page with auto-redirect\n */\nfunction renderSuccessPage(redirectUrl: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta http-equiv=\"refresh\" content=\"2;url=${escapeHtml(redirectUrl)}\">\n <title>Connected - Productive MCP</title>\n <style>\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n padding: 40px;\n width: 100%;\n max-width: 420px;\n text-align: center;\n }\n .success-icon {\n width: 64px;\n height: 64px;\n margin: 0 auto 24px;\n background: linear-gradient(135deg, #10b981 0%, #059669 100%);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .success-icon svg {\n width: 32px;\n height: 32px;\n stroke: white;\n }\n h1 {\n color: #1a1a2e;\n font-size: 24px;\n margin-bottom: 8px;\n }\n .message {\n color: #666;\n font-size: 14px;\n margin-bottom: 24px;\n }\n .spinner {\n width: 24px;\n height: 24px;\n border: 3px solid #e5e7eb;\n border-top-color: #667eea;\n border-radius: 50%;\n animation: spin 1s linear infinite;\n margin: 0 auto 16px;\n }\n @keyframes spin {\n to { transform: rotate(360deg); }\n }\n .redirect-text {\n color: #9ca3af;\n font-size: 13px;\n margin-bottom: 16px;\n }\n .manual-link {\n color: #667eea;\n text-decoration: none;\n font-size: 14px;\n }\n .manual-link:hover {\n text-decoration: underline;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"success-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M20 6L9 17L4 12\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n <h1>Successfully Connected!</h1>\n <p class=\"message\">Your Productive.io credentials have been verified.</p>\n <div class=\"spinner\"></div>\n <p class=\"redirect-text\">Redirecting to Claude Desktop...</p>\n <a href=\"${escapeHtml(redirectUrl)}\" class=\"manual-link\">Click here if not redirected automatically</a>\n </div>\n <script>\n // Redirect after a short delay (backup for meta refresh)\n setTimeout(function() {\n window.location.href = ${JSON.stringify(redirectUrl)};\n }, 2000);\n </script>\n</body>\n</html>`;\n}\n\n/**\n * Render error page\n */\nfunction renderErrorPage(message: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Error - Productive MCP</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #f3f4f6;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n padding: 40px;\n text-align: center;\n max-width: 400px;\n }\n h1 {\n color: #dc2626;\n margin-bottom: 16px;\n }\n p {\n color: #6b7280;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <h1>Error</h1>\n <p>${escapeHtml(message)}</p>\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oauth.js","names":[],"sources":["../src/oauth.ts"],"sourcesContent":["/**\n * OAuth 2.0 endpoints for Claude Desktop integration\n *\n * Implements OAuth 2.1 with PKCE as specified in the MCP authorization spec.\n * Uses stateless encrypted tokens - no server-side storage required.\n *\n * Spec: https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization\n *\n * Flow:\n * 1. Claude redirects user to /authorize with OAuth params (including PKCE)\n * 2. User enters Productive credentials in login form\n * 3. Server encrypts credentials + PKCE challenge into authorization code\n * 4. Redirects back to Claude with the code\n * 5. Claude exchanges code for access token via /token (with code_verifier)\n * 6. Server validates PKCE and returns access token\n */\n\nimport { defineHandler, getQuery, redirect, type H3Event } from 'h3';\nimport { createHash } from 'node:crypto';\n\nimport { createAuthToken } from './auth.js';\nimport { createAuthCode, decodeAuthCode } from './crypto.js';\n\n/**\n * OAuth metadata for discovery (RFC 8414)\n * GET /.well-known/oauth-authorization-server\n *\n * MCP clients MUST check this endpoint first for server capabilities.\n */\nexport const oauthMetadataHandler = defineHandler((event: H3Event) => {\n const host = event.req.headers.get('host') || 'localhost:3000';\n const protocol = event.req.headers.get('x-forwarded-proto') || 'http';\n const baseUrl = `${protocol}://${host}`;\n\n event.res.headers.set('Content-Type', 'application/json');\n event.res.headers.set('Cache-Control', 'public, max-age=3600');\n\n return {\n // Required fields per RFC 8414\n issuer: baseUrl,\n authorization_endpoint: `${baseUrl}/authorize`,\n token_endpoint: `${baseUrl}/token`,\n response_types_supported: ['code'],\n\n // OAuth 2.1 / MCP requirements\n grant_types_supported: ['authorization_code', 'refresh_token'],\n code_challenge_methods_supported: ['S256'],\n token_endpoint_auth_methods_supported: ['none'], // Public client\n\n // Optional but useful\n registration_endpoint: `${baseUrl}/register`,\n scopes_supported: ['productive'],\n service_documentation: 'https://github.com/studiometa/productive-tools',\n };\n});\n\n/**\n * Dynamic Client Registration endpoint (RFC 7591)\n * POST /register\n *\n * MCP servers SHOULD support DCR to allow clients to register automatically.\n * Since we use stateless tokens, we accept any registration and return\n * a generated client_id.\n */\nexport const registerHandler = defineHandler(async (event: H3Event) => {\n event.res.headers.set('Content-Type', 'application/json');\n\n let body: Record<string, unknown>;\n try {\n body = (await event.req.json()) as Record<string, unknown>;\n } catch {\n event.res.status = 400;\n return {\n error: 'invalid_request',\n error_description: 'Invalid JSON body',\n };\n }\n\n // Extract client metadata\n const clientName = (body.client_name as string) || 'MCP Client';\n const redirectUris = (body.redirect_uris as string[]) || [];\n\n // Generate a client_id based on the registration\n // Since we're stateless, we encode minimal info in the client_id\n const clientId = Buffer.from(\n JSON.stringify({\n name: clientName,\n ts: Date.now(),\n }),\n ).toString('base64url');\n\n event.res.status = 201;\n return {\n client_id: clientId,\n client_name: clientName,\n redirect_uris: redirectUris,\n token_endpoint_auth_method: 'none',\n grant_types: ['authorization_code', 'refresh_token'],\n response_types: ['code'],\n };\n});\n\n/**\n * Authorization endpoint - shows login form\n * GET /authorize\n */\nexport const authorizeGetHandler = defineHandler((event: H3Event) => {\n const query = getQuery(event);\n\n // Extract OAuth parameters\n const clientId = query.client_id as string;\n const redirectUri = query.redirect_uri as string;\n const state = query.state as string;\n const codeChallenge = query.code_challenge as string;\n const codeChallengeMethod = query.code_challenge_method as string;\n const scope = query.scope as string;\n\n // Validate required parameters per OAuth 2.1\n if (!redirectUri) {\n event.res.headers.set('Content-Type', 'text/html; charset=utf-8');\n event.res.status = 400;\n return renderErrorPage('Missing required parameter: redirect_uri');\n }\n\n // PKCE is REQUIRED for public clients per MCP spec\n if (!codeChallenge) {\n // Redirect back with error per OAuth spec\n const errorUrl = new URL(redirectUri);\n errorUrl.searchParams.set('error', 'invalid_request');\n errorUrl.searchParams.set('error_description', 'code_challenge is required');\n if (state) errorUrl.searchParams.set('state', state);\n return redirect(errorUrl.toString());\n }\n\n if (codeChallengeMethod && codeChallengeMethod !== 'S256') {\n const errorUrl = new URL(redirectUri);\n errorUrl.searchParams.set('error', 'invalid_request');\n errorUrl.searchParams.set('error_description', 'Only S256 code_challenge_method is supported');\n if (state) errorUrl.searchParams.set('state', state);\n return redirect(errorUrl.toString());\n }\n\n event.res.headers.set('Content-Type', 'text/html; charset=utf-8');\n\n // Render login form\n return renderLoginForm({\n clientId,\n redirectUri,\n state,\n codeChallenge,\n codeChallengeMethod: codeChallengeMethod || 'S256',\n scope,\n });\n});\n\n/**\n * Authorization endpoint - process login\n * POST /authorize\n */\nexport const authorizePostHandler = defineHandler(async (event: H3Event) => {\n const formData = await event.req.formData();\n const body = Object.fromEntries(formData.entries()) as Record<string, string>;\n\n const { orgId, apiToken, userId, redirectUri, state, codeChallenge, codeChallengeMethod } = body;\n\n // Validate redirect URI first (security requirement)\n if (!redirectUri) {\n event.res.headers.set('Content-Type', 'text/html; charset=utf-8');\n event.res.status = 400;\n return renderErrorPage('Missing redirect_uri parameter');\n }\n\n // Validate redirect URI format (must be HTTPS or localhost)\n try {\n const uri = new URL(redirectUri);\n const isLocalhost = uri.hostname === 'localhost' || uri.hostname === '127.0.0.1';\n const isHttps = uri.protocol === 'https:';\n if (!isLocalhost && !isHttps) {\n event.res.status = 400;\n return renderErrorPage('redirect_uri must be HTTPS or localhost');\n }\n } catch {\n event.res.status = 400;\n return renderErrorPage('Invalid redirect_uri format');\n }\n\n // Validate required credentials\n if (!orgId || !apiToken) {\n event.res.headers.set('Content-Type', 'text/html; charset=utf-8');\n return renderLoginForm({\n redirectUri,\n state,\n codeChallenge,\n codeChallengeMethod,\n error: 'Organization ID and API Token are required',\n });\n }\n\n // Create encrypted authorization code with PKCE challenge\n const code = createAuthCode({\n orgId,\n apiToken,\n userId: userId || undefined,\n codeChallenge,\n codeChallengeMethod: codeChallengeMethod || 'S256',\n });\n\n // Build redirect URL with authorization code\n const redirectUrl = new URL(redirectUri);\n redirectUrl.searchParams.set('code', code);\n if (state) {\n redirectUrl.searchParams.set('state', state);\n }\n\n // Redirect back to the OAuth client with the authorization code.\n // Strict OAuth clients may not follow HTML/meta-refresh or JavaScript redirects.\n return redirect(redirectUrl.toString());\n});\n\n/**\n * Token endpoint - exchange code for access token\n * POST /token\n *\n * Supports:\n * - authorization_code grant (with PKCE validation)\n * - refresh_token grant\n */\nexport const tokenHandler = defineHandler(async (event: H3Event) => {\n event.res.headers.set('Content-Type', 'application/json');\n\n let body: Record<string, string>;\n const contentType = event.req.headers.get('content-type') || '';\n\n if (contentType.includes('application/x-www-form-urlencoded')) {\n const rawText = await event.req.text();\n body = Object.fromEntries(new URLSearchParams(rawText));\n } else {\n body = (await event.req.json()) as Record<string, string>;\n }\n\n const { grant_type, code, code_verifier, refresh_token } = body;\n\n // Handle refresh token grant\n if (grant_type === 'refresh_token') {\n return handleRefreshToken(event, refresh_token);\n }\n\n // Validate authorization code grant\n if (grant_type !== 'authorization_code') {\n event.res.status = 400;\n return {\n error: 'unsupported_grant_type',\n error_description: 'Supported grant types: authorization_code, refresh_token',\n };\n }\n\n if (!code) {\n event.res.status = 400;\n return {\n error: 'invalid_request',\n error_description: 'Missing authorization code',\n };\n }\n\n if (!code_verifier) {\n event.res.status = 400;\n return {\n error: 'invalid_request',\n error_description: 'Missing code_verifier (PKCE required)',\n };\n }\n\n try {\n // Decode the authorization code\n const payload = decodeAuthCode(code);\n\n // Validate PKCE: SHA256(code_verifier) must equal code_challenge\n if (payload.codeChallenge) {\n const expectedChallenge = createS256Challenge(code_verifier);\n if (expectedChallenge !== payload.codeChallenge) {\n event.res.status = 400;\n return {\n error: 'invalid_grant',\n error_description: 'Invalid code_verifier',\n };\n }\n }\n\n // Create access token (base64 encoded credentials)\n const accessToken = createAuthToken({\n organizationId: payload.orgId,\n apiToken: payload.apiToken,\n userId: payload.userId,\n });\n\n // Create refresh token (encrypted credentials, longer expiry)\n const refreshToken = createAuthCode(\n {\n orgId: payload.orgId,\n apiToken: payload.apiToken,\n userId: payload.userId,\n },\n 86400 * 30, // 30 days\n );\n\n return {\n access_token: accessToken,\n token_type: 'Bearer',\n expires_in: 3600, // 1 hour (access tokens should be short-lived)\n refresh_token: refreshToken,\n };\n } catch (error) {\n event.res.status = 400;\n return {\n error: 'invalid_grant',\n error_description: error instanceof Error ? error.message : 'Invalid authorization code',\n };\n }\n});\n\n/**\n * Handle refresh token grant\n */\nfunction handleRefreshToken(event: H3Event, refreshToken: string | undefined) {\n if (!refreshToken) {\n event.res.status = 400;\n return {\n error: 'invalid_request',\n error_description: 'Missing refresh_token',\n };\n }\n\n try {\n // Decode refresh token (it's just an encrypted auth code with longer expiry)\n const payload = decodeAuthCode(refreshToken);\n\n // Create new access token\n const accessToken = createAuthToken({\n organizationId: payload.orgId,\n apiToken: payload.apiToken,\n userId: payload.userId,\n });\n\n // Create new refresh token (rotate for security)\n const newRefreshToken = createAuthCode(\n {\n orgId: payload.orgId,\n apiToken: payload.apiToken,\n userId: payload.userId,\n },\n 86400 * 30, // 30 days\n );\n\n return {\n access_token: accessToken,\n token_type: 'Bearer',\n expires_in: 3600,\n refresh_token: newRefreshToken,\n };\n } catch (error) {\n event.res.status = 400;\n return {\n error: 'invalid_grant',\n error_description: error instanceof Error ? error.message : 'Invalid refresh token',\n };\n }\n}\n\n/**\n * Create S256 PKCE challenge from verifier\n * SHA256(code_verifier) encoded as base64url\n */\nfunction createS256Challenge(codeVerifier: string): string {\n return createHash('sha256').update(codeVerifier).digest('base64url');\n}\n\n/**\n * Render the login form HTML\n */\nfunction renderLoginForm(params: {\n clientId?: string;\n redirectUri?: string;\n state?: string;\n codeChallenge?: string;\n codeChallengeMethod?: string;\n scope?: string;\n error?: string;\n}): string {\n const { redirectUri, state, codeChallenge, codeChallengeMethod, error } = params;\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Connect to Productive.io</title>\n <style>\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n padding: 40px;\n width: 100%;\n max-width: 420px;\n }\n .logo {\n text-align: center;\n margin-bottom: 24px;\n }\n .logo svg {\n width: 48px;\n height: 48px;\n }\n h1 {\n text-align: center;\n color: #1a1a2e;\n font-size: 24px;\n margin-bottom: 8px;\n }\n .subtitle {\n text-align: center;\n color: #666;\n font-size: 14px;\n margin-bottom: 32px;\n }\n .error {\n background: #fee2e2;\n border: 1px solid #fecaca;\n color: #dc2626;\n padding: 12px 16px;\n border-radius: 8px;\n margin-bottom: 24px;\n font-size: 14px;\n }\n .form-group {\n margin-bottom: 20px;\n }\n label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #374151;\n margin-bottom: 6px;\n }\n input {\n width: 100%;\n padding: 12px 16px;\n border: 1px solid #d1d5db;\n border-radius: 8px;\n font-size: 16px;\n transition: border-color 0.2s, box-shadow 0.2s;\n }\n input:focus {\n outline: none;\n border-color: #667eea;\n box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);\n }\n input::placeholder {\n color: #9ca3af;\n }\n .help-text {\n font-size: 12px;\n color: #6b7280;\n margin-top: 4px;\n }\n button {\n width: 100%;\n padding: 14px 24px;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: white;\n border: none;\n border-radius: 8px;\n font-size: 16px;\n font-weight: 600;\n cursor: pointer;\n transition: transform 0.2s, box-shadow 0.2s;\n }\n button:hover {\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);\n }\n button:active {\n transform: translateY(0);\n }\n .footer {\n text-align: center;\n margin-top: 24px;\n font-size: 12px;\n color: #9ca3af;\n }\n .footer a {\n color: #667eea;\n text-decoration: none;\n }\n .footer a:hover {\n text-decoration: underline;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"logo\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M12 2L2 7L12 12L22 7L12 2Z\" stroke=\"#667eea\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M2 17L12 22L22 17\" stroke=\"#764ba2\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M2 12L12 17L22 12\" stroke=\"#667eea\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n <h1>Connect to Productive.io</h1>\n <p class=\"subtitle\">Enter your Productive.io credentials to connect with Claude</p>\n \n ${error ? `<div class=\"error\">${escapeHtml(error)}</div>` : ''}\n \n <form method=\"POST\" action=\"/authorize\">\n <input type=\"hidden\" name=\"redirectUri\" value=\"${escapeHtml(redirectUri || '')}\">\n <input type=\"hidden\" name=\"state\" value=\"${escapeHtml(state || '')}\">\n <input type=\"hidden\" name=\"codeChallenge\" value=\"${escapeHtml(codeChallenge || '')}\">\n <input type=\"hidden\" name=\"codeChallengeMethod\" value=\"${escapeHtml(codeChallengeMethod || 'S256')}\">\n \n <div class=\"form-group\">\n <label for=\"orgId\">Organization ID *</label>\n <input type=\"text\" id=\"orgId\" name=\"orgId\" required placeholder=\"e.g., 12345\">\n <p class=\"help-text\">Found in Settings → API integrations</p>\n </div>\n \n <div class=\"form-group\">\n <label for=\"apiToken\">API Token *</label>\n <input type=\"password\" id=\"apiToken\" name=\"apiToken\" required placeholder=\"pk_...\">\n <p class=\"help-text\">Generate at Settings → API integrations → Generate new token</p>\n </div>\n \n <div class=\"form-group\">\n <label for=\"userId\">User ID (optional)</label>\n <input type=\"text\" id=\"userId\" name=\"userId\" placeholder=\"e.g., 67890\">\n <p class=\"help-text\">Required for creating time entries. Found in your profile URL.</p>\n </div>\n \n <button type=\"submit\">Connect to Productive</button>\n </form>\n \n <p class=\"footer\">\n Your credentials are encrypted and sent directly to Claude.<br>\n <a href=\"https://developer.productive.io\" target=\"_blank\">Productive.io API Documentation</a>\n </p>\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Render error page\n */\nfunction renderErrorPage(message: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Error - Productive MCP</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #f3f4f6;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n padding: 40px;\n text-align: center;\n max-width: 400px;\n }\n h1 {\n color: #dc2626;\n margin-bottom: 16px;\n }\n p {\n color: #6b7280;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <h1>Error</h1>\n <p>${escapeHtml(message)}</p>\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n"],"mappings":"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"}
|
package/dist/schema.d.ts
CHANGED
|
@@ -261,6 +261,37 @@ export declare const ProductiveToolInputSchema: z.ZodObject<{
|
|
|
261
261
|
}, z.core.$loose>>>;
|
|
262
262
|
}, z.core.$strip>;
|
|
263
263
|
export type ProductiveToolInput = z.infer<typeof ProductiveToolInputSchema>;
|
|
264
|
+
/**
|
|
265
|
+
* Full input schema for the raw api_read tool.
|
|
266
|
+
*/
|
|
267
|
+
export declare const ApiReadToolInputSchema: z.ZodObject<{
|
|
268
|
+
path: z.ZodString;
|
|
269
|
+
describe: z.ZodOptional<z.ZodBoolean>;
|
|
270
|
+
filter: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
271
|
+
include: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
272
|
+
sort: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
273
|
+
page: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
274
|
+
per_page: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
275
|
+
paginate: z.ZodOptional<z.ZodBoolean>;
|
|
276
|
+
max_pages: z.ZodOptional<z.ZodNumber>;
|
|
277
|
+
}, z.core.$strip>;
|
|
278
|
+
export type ApiReadToolInput = z.infer<typeof ApiReadToolInputSchema>;
|
|
279
|
+
/**
|
|
280
|
+
* Full input schema for the raw api_write tool.
|
|
281
|
+
*/
|
|
282
|
+
export declare const ApiWriteToolInputSchema: z.ZodObject<{
|
|
283
|
+
method: z.ZodEnum<{
|
|
284
|
+
DELETE: "DELETE";
|
|
285
|
+
PATCH: "PATCH";
|
|
286
|
+
POST: "POST";
|
|
287
|
+
PUT: "PUT";
|
|
288
|
+
}>;
|
|
289
|
+
path: z.ZodString;
|
|
290
|
+
body: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
291
|
+
confirm: z.ZodLiteral<true>;
|
|
292
|
+
dry_run: z.ZodOptional<z.ZodBoolean>;
|
|
293
|
+
}, z.core.$strip>;
|
|
294
|
+
export type ApiWriteToolInput = z.infer<typeof ApiWriteToolInputSchema>;
|
|
264
295
|
/**
|
|
265
296
|
* Validate and parse tool input
|
|
266
297
|
* Returns parsed data or throws with validation errors
|
|
@@ -274,5 +305,5 @@ export declare function safeValidateToolInput(input: unknown): ZodSafeParseResul
|
|
|
274
305
|
/**
|
|
275
306
|
* Format Zod validation errors for LLM consumption
|
|
276
307
|
*/
|
|
277
|
-
export declare function formatValidationErrors(error: ZodError<ProductiveToolInput>): string;
|
|
308
|
+
export declare function formatValidationErrors(error: ZodError<ProductiveToolInput | ApiReadToolInput | ApiWriteToolInput>): string;
|
|
278
309
|
//# sourceMappingURL=schema.d.ts.map
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,CAAC,EAAoB,KAAK,kBAAkB,EAAE,KAAK,QAAQ,EAAE,MAAM,KAAK,CAAC;AAGlF,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAMhF;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;EAAoB,CAAC;AAEhD;;;GAGG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;EAAkB,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;EAAuB,CAAC;AAMrD;;GAEG;AACH,eAAO,MAAM,OAAO,aAIgC,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,aAAa,aAKvB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,cAAc,aAKxB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,cAAc,aAGqD,CAAC;AAEjF;;GAEG;AACH,eAAO,MAAM,WAAW,aAG+C,CAAC;AAExE;;GAEG;AACH,eAAO,MAAM,cAAc,aAGuD,CAAC;AAEnF;;GAEG;AACH,eAAO,MAAM,WAAW,aAG+C,CAAC;AAExE;;GAEG;AACH,eAAO,MAAM,SAAS,aAIuC,CAAC;AAE9D;;GAEG;AACH,eAAO,MAAM,gBAAgB,aAKiE,CAAC;AAE/F;;GAEG;AACH,eAAO,MAAM,SAAS,2BAKiC,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,YAAY,2BAMsC,CAAC;AAEhE;;GAEG;AACH,eAAO,MAAM,YAAY,6BAKtB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,UAAU,aAMpB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,cAAc,wCAKxB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,YAAY,6BAKtB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,YAAY,yBAItB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,SAAS,aAAkD,CAAC;AAEzE;;GAEG;AACH,eAAO,MAAM,UAAU,aAIqB,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,SAAS,aAGgB,CAAC;AAMvC;;GAEG;AACH,eAAO,MAAM,YAAY,uDAGsB,CAAC;AAMhD;;;GAGG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqFpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,CAAC,EAAoB,KAAK,kBAAkB,EAAE,KAAK,QAAQ,EAAE,MAAM,KAAK,CAAC;AAGlF,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAMhF;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;EAAoB,CAAC;AAEhD;;;GAGG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;EAAkB,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;EAAuB,CAAC;AAMrD;;GAEG;AACH,eAAO,MAAM,OAAO,aAIgC,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,aAAa,aAKvB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,cAAc,aAKxB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,cAAc,aAGqD,CAAC;AAEjF;;GAEG;AACH,eAAO,MAAM,WAAW,aAG+C,CAAC;AAExE;;GAEG;AACH,eAAO,MAAM,cAAc,aAGuD,CAAC;AAEnF;;GAEG;AACH,eAAO,MAAM,WAAW,aAG+C,CAAC;AAExE;;GAEG;AACH,eAAO,MAAM,SAAS,aAIuC,CAAC;AAE9D;;GAEG;AACH,eAAO,MAAM,gBAAgB,aAKiE,CAAC;AAE/F;;GAEG;AACH,eAAO,MAAM,SAAS,2BAKiC,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,YAAY,2BAMsC,CAAC;AAEhE;;GAEG;AACH,eAAO,MAAM,YAAY,6BAKtB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,UAAU,aAMpB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,cAAc,wCAKxB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,YAAY,6BAKtB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,YAAY,yBAItB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,SAAS,aAAkD,CAAC;AAEzE;;GAEG;AACH,eAAO,MAAM,UAAU,aAIqB,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,SAAS,aAGgB,CAAC;AAMvC;;GAEG;AACH,eAAO,MAAM,YAAY,uDAGsB,CAAC;AAMhD;;;GAGG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqFpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E;;GAEG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;iBAUjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;GAEG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;iBAMlC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAMxE;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAErE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CAAC,mBAAmB,CAAC,CAE7F;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,QAAQ,CAAC,mBAAmB,GAAG,gBAAgB,GAAG,iBAAiB,CAAC,GAC1E,MAAM,CAOR"}
|
package/dist/server.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as VERSION } from "./version-
|
|
3
|
-
import "./
|
|
4
|
-
import { createHttpApp } from "./http.js";
|
|
2
|
+
import { t as VERSION } from "./version-Dm6m3p60.js";
|
|
3
|
+
import { t as createHttpApp } from "./http-B3J8ZV4I.js";
|
|
5
4
|
import { toNodeHandler } from "h3";
|
|
6
5
|
import { createServer } from "node:http";
|
|
6
|
+
//#region src/server.ts
|
|
7
7
|
/**
|
|
8
8
|
* Productive MCP Server - HTTP Transport
|
|
9
9
|
*
|
|
@@ -67,6 +67,7 @@ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith
|
|
|
67
67
|
console.error("Fatal error:", error);
|
|
68
68
|
process.exit(1);
|
|
69
69
|
});
|
|
70
|
+
//#endregion
|
|
70
71
|
export { startHttpServer };
|
|
71
72
|
|
|
72
73
|
//# sourceMappingURL=server.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { t as executeToolWithCredentials } from "./handlers-
|
|
1
|
+
import { t as executeToolWithCredentials } from "./handlers-B9FASjNJ.js";
|
|
2
2
|
import { STDIO_ONLY_TOOLS, TOOLS } from "./tools.js";
|
|
3
3
|
import { getConfig, setConfig } from "@studiometa/productive-api";
|
|
4
|
-
|
|
4
|
+
//#region src/prompts/definitions.ts
|
|
5
|
+
var PROMPT_DEFINITIONS = [
|
|
5
6
|
{
|
|
6
7
|
name: "end-of-day",
|
|
7
8
|
description: "Compose an end-of-day standup message based on your activity today",
|
|
@@ -64,6 +65,8 @@ const PROMPT_DEFINITIONS = [
|
|
|
64
65
|
]
|
|
65
66
|
}
|
|
66
67
|
];
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/prompts/handlers.ts
|
|
67
70
|
/**
|
|
68
71
|
* Build the end-of-day standup prompt messages
|
|
69
72
|
*/
|
|
@@ -254,6 +257,8 @@ function getPromptMessages(name, args) {
|
|
|
254
257
|
default: throw new Error(`Unknown prompt: ${name}`);
|
|
255
258
|
}
|
|
256
259
|
}
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/stdio.ts
|
|
257
262
|
/**
|
|
258
263
|
* Get all available tools (including stdio-only configuration tools)
|
|
259
264
|
*/
|
|
@@ -346,6 +351,7 @@ async function handlePrompt(name, args) {
|
|
|
346
351
|
if (name === "setup_productive") return handleSetupPrompt();
|
|
347
352
|
return getPromptMessages(name, args);
|
|
348
353
|
}
|
|
354
|
+
//#endregion
|
|
349
355
|
export { handlePrompt as a, handleGetConfigTool as i, getAvailableTools as n, handleSetupPrompt as o, handleConfigureTool as r, handleToolCall as s, getAvailablePrompts as t };
|
|
350
356
|
|
|
351
|
-
//# sourceMappingURL=stdio-
|
|
357
|
+
//# sourceMappingURL=stdio-BpKd5pcS.js.map
|