@studiometa/productive-mcp 0.3.0 → 0.4.6

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 CHANGED
@@ -10,6 +10,7 @@ MCP (Model Context Protocol) server for [Productive.io](https://productive.io) A
10
10
  - ✅ Full Productive.io API access via MCP
11
11
  - 🔧 Support for projects, tasks, time entries, services, and people
12
12
  - 🔐 Two modes: local (stdio) and remote (HTTP)
13
+ - 🔑 **OAuth 2.0 support** for Claude Desktop custom connectors
13
14
  - 🌐 Deploy once, share with your team via Claude Desktop custom connectors
14
15
  - 🐳 Docker-ready for easy deployment
15
16
  - 📦 Built on [@studiometa/productive-cli](../productive-cli)
@@ -83,9 +84,14 @@ Deploy once, share with your entire team via Claude Desktop's **custom connector
83
84
 
84
85
  ### How It Works
85
86
 
87
+ The server supports **OAuth 2.0** for seamless Claude Desktop integration:
88
+
86
89
  1. Deploy the HTTP server to a URL (e.g., `https://productive.mcp.example.com`)
87
- 2. Each team member generates their own Bearer token with their Productive credentials
88
- 3. Team members add the custom connector in Claude Desktop with their personal token
90
+ 2. Add the custom connector in Claude Desktop with OAuth enabled
91
+ 3. When connecting, users are presented with a login form to enter their Productive credentials
92
+ 4. Credentials are securely encrypted and exchanged via OAuth tokens
93
+
94
+ No central credential storage required - each user's credentials are encrypted directly in their OAuth token.
89
95
 
90
96
  ### Deploy the Server
91
97
 
@@ -128,22 +134,21 @@ services:
128
134
  environment:
129
135
  PORT: 3000
130
136
  HOST: 0.0.0.0
137
+ OAUTH_SECRET: "your-random-secret-here" # Required for production!
131
138
  ```
132
139
 
133
- ### Generate Your Token
140
+ ### Environment Variables
134
141
 
135
- Each team member generates their own token containing their Productive credentials:
136
-
137
- ```bash
138
- # Format: base64(organizationId:apiToken:userId)
139
- echo -n "YOUR_ORG_ID:YOUR_API_TOKEN:YOUR_USER_ID" | base64
140
- ```
142
+ | Variable | Required | Description |
143
+ |----------|----------|-------------|
144
+ | `PORT` | No | Server port (default: 3000) |
145
+ | `HOST` | No | Bind address (default: 0.0.0.0) |
146
+ | `OAUTH_SECRET` | **Yes (production)** | Secret key for encrypting OAuth tokens |
141
147
 
142
- Example:
143
- ```bash
144
- echo -n "12345:pk_abc123xyz:67890" | base64
145
- # Output: MTIzNDU6cGtfYWJjMTIzeHl6OjY3ODkw
146
- ```
148
+ > ⚠️ **Important**: Always set `OAUTH_SECRET` in production. Generate a random secret:
149
+ > ```bash
150
+ > openssl rand -base64 32
151
+ > ```
147
152
 
148
153
  ### Configure Claude Desktop Custom Connector
149
154
 
@@ -153,10 +158,25 @@ echo -n "12345:pk_abc123xyz:67890" | base64
153
158
  4. Configure:
154
159
  - **Name**: `Productive`
155
160
  - **Remote MCP server URL**: `https://productive.mcp.example.com/mcp`
156
- - Leave OAuth fields empty (we use Bearer token)
157
- 5. When making requests, Claude will include your token in the `Authorization` header
161
+ - **Authorization URL**: `https://productive.mcp.example.com/authorize`
162
+ - **Token URL**: `https://productive.mcp.example.com/token`
163
+ 5. Claude will redirect you to a login form to enter your Productive credentials
164
+ 6. After login, you're connected and can start using Productive tools
165
+
166
+ ### Alternative: Manual Bearer Token
167
+
168
+ If you prefer not to use OAuth, you can generate a Bearer token manually:
158
169
 
159
- > **Note**: As of now, Claude Desktop custom connectors may require OAuth. If Bearer token auth isn't supported directly, you can use a reverse proxy to inject the Authorization header, or wait for Claude Desktop to support custom headers.
170
+ ```bash
171
+ # Format: base64(organizationId:apiToken:userId)
172
+ echo -n "YOUR_ORG_ID:YOUR_API_TOKEN:YOUR_USER_ID" | base64
173
+ ```
174
+
175
+ Example:
176
+ ```bash
177
+ echo -n "12345:pk_abc123xyz:67890" | base64
178
+ # Output: MTIzNDU6cGtfYWJjMTIzeHl6OjY3ODkw
179
+ ```
160
180
 
161
181
  ### Server Endpoints
162
182
 
@@ -165,6 +185,10 @@ echo -n "12345:pk_abc123xyz:67890" | base64
165
185
  | `/mcp` | POST | MCP JSON-RPC endpoint |
166
186
  | `/health` | GET | Health check |
167
187
  | `/` | GET | Server info |
188
+ | `/authorize` | GET | OAuth authorization (login form) |
189
+ | `/authorize` | POST | OAuth authorization (process login) |
190
+ | `/token` | POST | OAuth token exchange |
191
+ | `/.well-known/oauth-authorization-server` | GET | OAuth metadata |
168
192
 
169
193
  ---
170
194
 
@@ -326,6 +350,9 @@ productive-mcp/
326
350
  ├── src/
327
351
  │ ├── index.ts # Stdio transport (local mode)
328
352
  │ ├── server.ts # HTTP transport (remote mode)
353
+ │ ├── http.ts # HTTP routes and MCP endpoint
354
+ │ ├── oauth.ts # OAuth 2.0 endpoints
355
+ │ ├── crypto.ts # Encryption for stateless OAuth tokens
329
356
  │ ├── tools.ts # Tool definitions (shared)
330
357
  │ ├── handlers.ts # Tool execution (shared)
331
358
  │ └── auth.ts # Bearer token parsing
@@ -333,6 +360,20 @@ productive-mcp/
333
360
  └── README.md
334
361
  ```
335
362
 
363
+ ### OAuth Flow (Stateless)
364
+
365
+ The OAuth implementation is **stateless** - no database or session storage required:
366
+
367
+ 1. User visits `/authorize` → sees login form
368
+ 2. User enters Productive credentials (org ID, API token, user ID)
369
+ 3. Server encrypts credentials into the authorization code using `OAUTH_SECRET`
370
+ 4. Redirects back to Claude with the encrypted code
371
+ 5. Claude calls `/token` with the code
372
+ 6. Server decrypts the code → returns access token (base64 credentials)
373
+ 7. All MCP requests include this token in the `Authorization: Bearer` header
374
+
375
+ This approach keeps the server stateless while securely passing credentials through the OAuth flow.
376
+
336
377
  ## Related Packages
337
378
 
338
379
  - [@studiometa/productive-cli](../productive-cli) - CLI tool for Productive.io
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Cryptographic utilities for stateless OAuth tokens
3
+ *
4
+ * Uses AES-256-GCM for authenticated encryption.
5
+ * The authorization code contains encrypted credentials that can be
6
+ * decrypted without server-side storage.
7
+ */
8
+ /**
9
+ * Get the encryption secret from environment or generate a default
10
+ * In production, OAUTH_SECRET should always be set
11
+ */
12
+ export declare function getSecret(): string;
13
+ /**
14
+ * Encrypt data using AES-256-GCM
15
+ *
16
+ * Output format: base64(salt + iv + authTag + ciphertext)
17
+ *
18
+ * @param plaintext - Data to encrypt
19
+ * @param secret - Encryption secret (defaults to OAUTH_SECRET env var)
20
+ * @returns Base64-encoded encrypted data
21
+ */
22
+ export declare function encrypt(plaintext: string, secret?: string): string;
23
+ /**
24
+ * Decrypt data encrypted with encrypt()
25
+ *
26
+ * @param ciphertext - Base64-encoded encrypted data
27
+ * @param secret - Encryption secret (defaults to OAUTH_SECRET env var)
28
+ * @returns Decrypted plaintext
29
+ * @throws Error if decryption fails (invalid data or wrong secret)
30
+ */
31
+ export declare function decrypt(ciphertext: string, secret?: string): string;
32
+ /**
33
+ * Authorization code payload structure
34
+ */
35
+ export interface AuthCodePayload {
36
+ orgId: string;
37
+ apiToken: string;
38
+ userId?: string;
39
+ codeChallenge?: string;
40
+ codeChallengeMethod?: string;
41
+ }
42
+ /**
43
+ * Create an encrypted authorization code containing credentials and PKCE challenge
44
+ *
45
+ * @param credentials - Object with orgId, apiToken, userId, and optional PKCE params
46
+ * @param expiresInSeconds - Code expiration time (default: 5 minutes)
47
+ * @returns Encrypted authorization code
48
+ */
49
+ export declare function createAuthCode(credentials: AuthCodePayload, expiresInSeconds?: number): string;
50
+ /**
51
+ * Decode and validate an authorization code
52
+ *
53
+ * @param code - Encrypted authorization code
54
+ * @returns Decoded payload with credentials and PKCE challenge
55
+ * @throws Error if code is invalid or expired
56
+ */
57
+ export declare function decodeAuthCode(code: string): AuthCodePayload;
58
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAgBH;;;GAGG;AACH,wBAAgB,SAAS,IAAI,MAAM,CASlC;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,GAAE,MAAoB,GAAG,MAAM,CAa/E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,GAAE,MAAoB,GAAG,MAAM,CAwBhF;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,eAAe,EAC5B,gBAAgB,GAAE,MAAY,GAC7B,MAAM,CAMR;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAc5D"}
package/dist/crypto.js ADDED
@@ -0,0 +1,73 @@
1
+ import { randomBytes, createCipheriv, createDecipheriv, scryptSync } from "node:crypto";
2
+ const ALGORITHM = "aes-256-gcm";
3
+ const IV_LENGTH = 12;
4
+ const AUTH_TAG_LENGTH = 16;
5
+ const SALT_LENGTH = 16;
6
+ function deriveKey(password, salt) {
7
+ return scryptSync(password, salt, 32);
8
+ }
9
+ function getSecret() {
10
+ const secret = process.env.OAUTH_SECRET;
11
+ if (!secret) {
12
+ console.warn(
13
+ "WARNING: OAUTH_SECRET not set. Using default secret. Set OAUTH_SECRET in production!"
14
+ );
15
+ return "productive-mcp-default-secret-change-me";
16
+ }
17
+ return secret;
18
+ }
19
+ function encrypt(plaintext, secret = getSecret()) {
20
+ const salt = randomBytes(SALT_LENGTH);
21
+ const key = deriveKey(secret, salt);
22
+ const iv = randomBytes(IV_LENGTH);
23
+ const cipher = createCipheriv(ALGORITHM, key, iv);
24
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
25
+ const authTag = cipher.getAuthTag();
26
+ const combined = Buffer.concat([salt, iv, authTag, encrypted]);
27
+ return combined.toString("base64url");
28
+ }
29
+ function decrypt(ciphertext, secret = getSecret()) {
30
+ try {
31
+ const combined = Buffer.from(ciphertext, "base64url");
32
+ const salt = combined.subarray(0, SALT_LENGTH);
33
+ const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
34
+ const authTag = combined.subarray(
35
+ SALT_LENGTH + IV_LENGTH,
36
+ SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH
37
+ );
38
+ const encrypted = combined.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
39
+ const key = deriveKey(secret, salt);
40
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
41
+ decipher.setAuthTag(authTag);
42
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
43
+ return decrypted.toString("utf8");
44
+ } catch {
45
+ throw new Error("Decryption failed: invalid token or secret");
46
+ }
47
+ }
48
+ function createAuthCode(credentials, expiresInSeconds = 300) {
49
+ const payload = {
50
+ ...credentials,
51
+ exp: Date.now() + expiresInSeconds * 1e3
52
+ };
53
+ return encrypt(JSON.stringify(payload));
54
+ }
55
+ function decodeAuthCode(code) {
56
+ const payload = JSON.parse(decrypt(code));
57
+ if (payload.exp && Date.now() > payload.exp) {
58
+ throw new Error("Authorization code expired");
59
+ }
60
+ const { orgId, apiToken, userId, codeChallenge, codeChallengeMethod } = payload;
61
+ if (!orgId || !apiToken) {
62
+ throw new Error("Invalid authorization code: missing credentials");
63
+ }
64
+ return { orgId, apiToken, userId, codeChallenge, codeChallengeMethod };
65
+ }
66
+ export {
67
+ createAuthCode,
68
+ decodeAuthCode,
69
+ decrypt,
70
+ encrypt,
71
+ getSecret
72
+ };
73
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sources":["../src/crypto.ts"],"sourcesContent":["/**\n * Cryptographic utilities for stateless OAuth tokens\n *\n * Uses AES-256-GCM for authenticated encryption.\n * The authorization code contains encrypted credentials that can be\n * decrypted without server-side storage.\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';\n\nconst ALGORITHM = 'aes-256-gcm';\nconst IV_LENGTH = 12; // GCM recommended IV length\nconst AUTH_TAG_LENGTH = 16;\nconst SALT_LENGTH = 16;\n\n/**\n * Derive a 256-bit key from a password using scrypt\n */\nfunction deriveKey(password: string, salt: Buffer): Buffer {\n return scryptSync(password, salt, 32);\n}\n\n/**\n * Get the encryption secret from environment or generate a default\n * In production, OAUTH_SECRET should always be set\n */\nexport function getSecret(): string {\n const secret = process.env.OAUTH_SECRET;\n if (!secret) {\n console.warn(\n 'WARNING: OAUTH_SECRET not set. Using default secret. Set OAUTH_SECRET in production!'\n );\n return 'productive-mcp-default-secret-change-me';\n }\n return secret;\n}\n\n/**\n * Encrypt data using AES-256-GCM\n *\n * Output format: base64(salt + iv + authTag + ciphertext)\n *\n * @param plaintext - Data to encrypt\n * @param secret - Encryption secret (defaults to OAUTH_SECRET env var)\n * @returns Base64-encoded encrypted data\n */\nexport function encrypt(plaintext: string, secret: string = getSecret()): string {\n const salt = randomBytes(SALT_LENGTH);\n const key = deriveKey(secret, salt);\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv(ALGORITHM, key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const authTag = cipher.getAuthTag();\n\n // Combine: salt + iv + authTag + ciphertext\n const combined = Buffer.concat([salt, iv, authTag, encrypted]);\n\n return combined.toString('base64url');\n}\n\n/**\n * Decrypt data encrypted with encrypt()\n *\n * @param ciphertext - Base64-encoded encrypted data\n * @param secret - Encryption secret (defaults to OAUTH_SECRET env var)\n * @returns Decrypted plaintext\n * @throws Error if decryption fails (invalid data or wrong secret)\n */\nexport function decrypt(ciphertext: string, secret: string = getSecret()): string {\n try {\n const combined = Buffer.from(ciphertext, 'base64url');\n\n // Extract components\n const salt = combined.subarray(0, SALT_LENGTH);\n const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const authTag = combined.subarray(\n SALT_LENGTH + IV_LENGTH,\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH\n );\n const encrypted = combined.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);\n\n const key = deriveKey(secret, salt);\n\n const decipher = createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);\n\n return decrypted.toString('utf8');\n } catch {\n throw new Error('Decryption failed: invalid token or secret');\n }\n}\n\n/**\n * Authorization code payload structure\n */\nexport interface AuthCodePayload {\n orgId: string;\n apiToken: string;\n userId?: string;\n codeChallenge?: string;\n codeChallengeMethod?: string;\n}\n\n/**\n * Create an encrypted authorization code containing credentials and PKCE challenge\n *\n * @param credentials - Object with orgId, apiToken, userId, and optional PKCE params\n * @param expiresInSeconds - Code expiration time (default: 5 minutes)\n * @returns Encrypted authorization code\n */\nexport function createAuthCode(\n credentials: AuthCodePayload,\n expiresInSeconds: number = 300\n): string {\n const payload = {\n ...credentials,\n exp: Date.now() + expiresInSeconds * 1000,\n };\n return encrypt(JSON.stringify(payload));\n}\n\n/**\n * Decode and validate an authorization code\n *\n * @param code - Encrypted authorization code\n * @returns Decoded payload with credentials and PKCE challenge\n * @throws Error if code is invalid or expired\n */\nexport function decodeAuthCode(code: string): AuthCodePayload {\n const payload = JSON.parse(decrypt(code));\n\n if (payload.exp && Date.now() > payload.exp) {\n throw new Error('Authorization code expired');\n }\n\n const { orgId, apiToken, userId, codeChallenge, codeChallengeMethod } = payload;\n\n if (!orgId || !apiToken) {\n throw new Error('Invalid authorization code: missing credentials');\n }\n\n return { orgId, apiToken, userId, codeChallenge, codeChallengeMethod };\n}\n"],"names":[],"mappings":";AAUA,MAAM,YAAY;AAClB,MAAM,YAAY;AAClB,MAAM,kBAAkB;AACxB,MAAM,cAAc;AAKpB,SAAS,UAAU,UAAkB,MAAsB;AACzD,SAAO,WAAW,UAAU,MAAM,EAAE;AACtC;AAMO,SAAS,YAAoB;AAClC,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,QAAQ;AACX,YAAQ;AAAA,MACN;AAAA,IAAA;AAEF,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAWO,SAAS,QAAQ,WAAmB,SAAiB,aAAqB;AAC/E,QAAM,OAAO,YAAY,WAAW;AACpC,QAAM,MAAM,UAAU,QAAQ,IAAI;AAClC,QAAM,KAAK,YAAY,SAAS;AAEhC,QAAM,SAAS,eAAe,WAAW,KAAK,EAAE;AAChD,QAAM,YAAY,OAAO,OAAO,CAAC,OAAO,OAAO,WAAW,MAAM,GAAG,OAAO,MAAA,CAAO,CAAC;AAClF,QAAM,UAAU,OAAO,WAAA;AAGvB,QAAM,WAAW,OAAO,OAAO,CAAC,MAAM,IAAI,SAAS,SAAS,CAAC;AAE7D,SAAO,SAAS,SAAS,WAAW;AACtC;AAUO,SAAS,QAAQ,YAAoB,SAAiB,aAAqB;AAChF,MAAI;AACF,UAAM,WAAW,OAAO,KAAK,YAAY,WAAW;AAGpD,UAAM,OAAO,SAAS,SAAS,GAAG,WAAW;AAC7C,UAAM,KAAK,SAAS,SAAS,aAAa,cAAc,SAAS;AACjE,UAAM,UAAU,SAAS;AAAA,MACvB,cAAc;AAAA,MACd,cAAc,YAAY;AAAA,IAAA;AAE5B,UAAM,YAAY,SAAS,SAAS,cAAc,YAAY,eAAe;AAE7E,UAAM,MAAM,UAAU,QAAQ,IAAI;AAElC,UAAM,WAAW,iBAAiB,WAAW,KAAK,EAAE;AACpD,aAAS,WAAW,OAAO;AAE3B,UAAM,YAAY,OAAO,OAAO,CAAC,SAAS,OAAO,SAAS,GAAG,SAAS,MAAA,CAAO,CAAC;AAE9E,WAAO,UAAU,SAAS,MAAM;AAAA,EAClC,QAAQ;AACN,UAAM,IAAI,MAAM,4CAA4C;AAAA,EAC9D;AACF;AAoBO,SAAS,eACd,aACA,mBAA2B,KACnB;AACR,QAAM,UAAU;AAAA,IACd,GAAG;AAAA,IACH,KAAK,KAAK,IAAA,IAAQ,mBAAmB;AAAA,EAAA;AAEvC,SAAO,QAAQ,KAAK,UAAU,OAAO,CAAC;AACxC;AASO,SAAS,eAAe,MAA+B;AAC5D,QAAM,UAAU,KAAK,MAAM,QAAQ,IAAI,CAAC;AAExC,MAAI,QAAQ,OAAO,KAAK,IAAA,IAAQ,QAAQ,KAAK;AAC3C,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AAEA,QAAM,EAAE,OAAO,UAAU,QAAQ,eAAe,wBAAwB;AAExE,MAAI,CAAC,SAAS,CAAC,UAAU;AACvB,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AAEA,SAAO,EAAE,OAAO,UAAU,QAAQ,eAAe,oBAAA;AACnD;"}
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAUvD,MAAM,MAAM,UAAU,GAAG,cAAc,CAAC;AAqBxC;;;;;;;GAOG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,WAAW,EAAE,qBAAqB,GACjC,OAAO,CAAC,UAAU,CAAC,CA0GrB"}
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAUvD,MAAM,MAAM,UAAU,GAAG,cAAc,CAAC;AAqBxC;;;;;;;GAOG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,WAAW,EAAE,qBAAqB,GACjC,OAAO,CAAC,UAAU,CAAC,CA4GrB"}
package/dist/handlers.js CHANGED
@@ -42,8 +42,9 @@ function errorResult(message) {
42
42
  }
43
43
  async function executeToolWithCredentials(name, args, credentials) {
44
44
  const api = new ProductiveApi({
45
- apiToken: credentials.apiToken,
46
- organizationId: credentials.organizationId
45
+ token: credentials.apiToken,
46
+ "org-id": credentials.organizationId,
47
+ "user-id": credentials.userId
47
48
  });
48
49
  try {
49
50
  switch (name) {
@@ -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\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 * Format time entry for agent consumption\n */\nexport function formatTimeEntry(\n entry: JsonApiResource,\n _included?: JsonApiResource[]\n): Record<string, unknown> {\n return cliFormatTimeEntry(entry, MCP_FORMAT_OPTIONS);\n}\n\n/**\n * Format project for agent consumption\n */\nexport function formatProject(\n project: JsonApiResource,\n _included?: JsonApiResource[]\n): Record<string, unknown> {\n return cliFormatProject(project, MCP_FORMAT_OPTIONS);\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 included?: JsonApiResource[]\n): Record<string, unknown> {\n return cliFormatTask(task, { ...MCP_FORMAT_OPTIONS, included });\n}\n\n/**\n * Format person for agent consumption\n */\nexport function formatPerson(\n person: JsonApiResource,\n _included?: JsonApiResource[]\n): Record<string, unknown> {\n return cliFormatPerson(person, MCP_FORMAT_OPTIONS);\n}\n\n/**\n * Format service for agent consumption\n */\nexport function formatService(\n service: JsonApiResource,\n _included?: JsonApiResource[]\n): Record<string, unknown> {\n return cliFormatService(service, MCP_FORMAT_OPTIONS);\n}\n\n/**\n * Format list response with pagination\n *\n * @param data - Array of JSON:API resources\n * @param formatter - Formatter function (item, included?) => T\n * @param meta - Pagination metadata\n * @param included - Included resources for relationship resolution\n */\nexport function formatListResponse<T>(\n data: JsonApiResource[],\n formatter: (item: JsonApiResource, included?: JsonApiResource[]) => T,\n meta?: JsonApiMeta,\n included?: JsonApiResource[]\n): { data: T[]; meta?: FormattedPagination } {\n // Create a wrapper that converts (item, options?) signature to (item, included?) signature\n const wrappedFormatter = (item: JsonApiResource, _options?: FormatOptions) => {\n return formatter(item, included);\n };\n\n const result = cliFormatListResponse(data, wrappedFormatter, meta, {\n ...MCP_FORMAT_OPTIONS,\n 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\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} from './formatters.js';\n\nexport type ToolResult = CallToolResult;\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 * Execute a tool with the given credentials and arguments\n *\n * @param name - Tool name\n * @param args - Tool arguments\n * @param credentials - Productive API credentials\n * @returns Tool execution result\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 apiToken: credentials.apiToken,\n organizationId: credentials.organizationId,\n } as Record<string, string>);\n\n try {\n switch (name) {\n case 'productive_list_projects': {\n const result = await api.getProjects(args as Parameters<typeof api.getProjects>[0]);\n return jsonResult(formatListResponse(result.data, formatProject, result.meta));\n }\n\n case 'productive_get_project': {\n const result = await api.getProject((args as { id: string }).id);\n return jsonResult(formatProject(result.data));\n }\n\n case 'productive_list_time_entries': {\n const result = await api.getTimeEntries(args as Parameters<typeof api.getTimeEntries>[0]);\n return jsonResult(formatListResponse(result.data, formatTimeEntry, result.meta));\n }\n\n case 'productive_get_time_entry': {\n const result = await api.getTimeEntry((args as { id: string }).id);\n return jsonResult(formatTimeEntry(result.data));\n }\n\n case 'productive_create_time_entry': {\n const result = await api.createTimeEntry(\n args as Parameters<typeof api.createTimeEntry>[0]\n );\n return jsonResult({\n success: true,\n ...formatTimeEntry(result.data),\n });\n }\n\n case 'productive_update_time_entry': {\n const { id, ...data } = args as { id: string } & Record<string, unknown>;\n const result = await api.updateTimeEntry(id, data as Parameters<typeof api.updateTimeEntry>[1]);\n return jsonResult({\n success: true,\n ...formatTimeEntry(result.data),\n });\n }\n\n case 'productive_delete_time_entry': {\n await api.deleteTimeEntry((args as { id: string }).id);\n return jsonResult({ success: true, message: 'Time entry deleted' });\n }\n\n case 'productive_list_tasks': {\n const params = args as Parameters<typeof api.getTasks>[0] || {};\n // Always include project and company for context\n params.include = ['project', 'project.company'];\n const result = await api.getTasks(params);\n return jsonResult(formatListResponse(\n result.data,\n formatTask,\n result.meta,\n result.included\n ));\n }\n\n case 'productive_get_task': {\n const result = await api.getTask((args as { id: string }).id, {\n include: ['project', 'project.company'],\n });\n return jsonResult(formatTask(result.data, result.included));\n }\n\n case 'productive_list_services': {\n const result = await api.getServices(args as Parameters<typeof api.getServices>[0]);\n return jsonResult(formatListResponse(result.data, formatService, result.meta));\n }\n\n case 'productive_list_people': {\n const result = await api.getPeople(args as Parameters<typeof api.getPeople>[0]);\n return jsonResult(formatListResponse(result.data, formatPerson, result.meta));\n }\n\n case 'productive_get_person': {\n const result = await api.getPerson((args as { id: string }).id);\n return jsonResult(formatPerson(result.data));\n }\n\n case 'productive_get_current_user': {\n if (credentials.userId) {\n const result = await api.getPerson(credentials.userId);\n return jsonResult(formatPerson(result.data));\n }\n return jsonResult({\n message: 'User ID not configured. Set userId in credentials to use this tool.',\n organizationId: credentials.organizationId,\n });\n }\n\n default:\n return errorResult(`Unknown tool: ${name}`);\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return errorResult(message);\n }\n}\n"],"names":["cliFormatTimeEntry","cliFormatProject","cliFormatTask","cliFormatPerson","cliFormatService","cliFormatListResponse"],"mappings":"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"}
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\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 * Format time entry for agent consumption\n */\nexport function formatTimeEntry(\n entry: JsonApiResource,\n _included?: JsonApiResource[]\n): Record<string, unknown> {\n return cliFormatTimeEntry(entry, MCP_FORMAT_OPTIONS);\n}\n\n/**\n * Format project for agent consumption\n */\nexport function formatProject(\n project: JsonApiResource,\n _included?: JsonApiResource[]\n): Record<string, unknown> {\n return cliFormatProject(project, MCP_FORMAT_OPTIONS);\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 included?: JsonApiResource[]\n): Record<string, unknown> {\n return cliFormatTask(task, { ...MCP_FORMAT_OPTIONS, included });\n}\n\n/**\n * Format person for agent consumption\n */\nexport function formatPerson(\n person: JsonApiResource,\n _included?: JsonApiResource[]\n): Record<string, unknown> {\n return cliFormatPerson(person, MCP_FORMAT_OPTIONS);\n}\n\n/**\n * Format service for agent consumption\n */\nexport function formatService(\n service: JsonApiResource,\n _included?: JsonApiResource[]\n): Record<string, unknown> {\n return cliFormatService(service, MCP_FORMAT_OPTIONS);\n}\n\n/**\n * Format list response with pagination\n *\n * @param data - Array of JSON:API resources\n * @param formatter - Formatter function (item, included?) => T\n * @param meta - Pagination metadata\n * @param included - Included resources for relationship resolution\n */\nexport function formatListResponse<T>(\n data: JsonApiResource[],\n formatter: (item: JsonApiResource, included?: JsonApiResource[]) => T,\n meta?: JsonApiMeta,\n included?: JsonApiResource[]\n): { data: T[]; meta?: FormattedPagination } {\n // Create a wrapper that converts (item, options?) signature to (item, included?) signature\n const wrappedFormatter = (item: JsonApiResource, _options?: FormatOptions) => {\n return formatter(item, included);\n };\n\n const result = cliFormatListResponse(data, wrappedFormatter, meta, {\n ...MCP_FORMAT_OPTIONS,\n 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\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} from './formatters.js';\n\nexport type ToolResult = CallToolResult;\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 * Execute a tool with the given credentials and arguments\n *\n * @param name - Tool name\n * @param args - Tool arguments\n * @param credentials - Productive API credentials\n * @returns Tool execution result\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 // Note: getConfig expects CLI-style keys (token, org-id) not camelCase\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 try {\n switch (name) {\n case 'productive_list_projects': {\n const result = await api.getProjects(args as Parameters<typeof api.getProjects>[0]);\n return jsonResult(formatListResponse(result.data, formatProject, result.meta));\n }\n\n case 'productive_get_project': {\n const result = await api.getProject((args as { id: string }).id);\n return jsonResult(formatProject(result.data));\n }\n\n case 'productive_list_time_entries': {\n const result = await api.getTimeEntries(args as Parameters<typeof api.getTimeEntries>[0]);\n return jsonResult(formatListResponse(result.data, formatTimeEntry, result.meta));\n }\n\n case 'productive_get_time_entry': {\n const result = await api.getTimeEntry((args as { id: string }).id);\n return jsonResult(formatTimeEntry(result.data));\n }\n\n case 'productive_create_time_entry': {\n const result = await api.createTimeEntry(\n args as Parameters<typeof api.createTimeEntry>[0]\n );\n return jsonResult({\n success: true,\n ...formatTimeEntry(result.data),\n });\n }\n\n case 'productive_update_time_entry': {\n const { id, ...data } = args as { id: string } & Record<string, unknown>;\n const result = await api.updateTimeEntry(id, data as Parameters<typeof api.updateTimeEntry>[1]);\n return jsonResult({\n success: true,\n ...formatTimeEntry(result.data),\n });\n }\n\n case 'productive_delete_time_entry': {\n await api.deleteTimeEntry((args as { id: string }).id);\n return jsonResult({ success: true, message: 'Time entry deleted' });\n }\n\n case 'productive_list_tasks': {\n const params = args as Parameters<typeof api.getTasks>[0] || {};\n // Always include project and company for context\n params.include = ['project', 'project.company'];\n const result = await api.getTasks(params);\n return jsonResult(formatListResponse(\n result.data,\n formatTask,\n result.meta,\n result.included\n ));\n }\n\n case 'productive_get_task': {\n const result = await api.getTask((args as { id: string }).id, {\n include: ['project', 'project.company'],\n });\n return jsonResult(formatTask(result.data, result.included));\n }\n\n case 'productive_list_services': {\n const result = await api.getServices(args as Parameters<typeof api.getServices>[0]);\n return jsonResult(formatListResponse(result.data, formatService, result.meta));\n }\n\n case 'productive_list_people': {\n const result = await api.getPeople(args as Parameters<typeof api.getPeople>[0]);\n return jsonResult(formatListResponse(result.data, formatPerson, result.meta));\n }\n\n case 'productive_get_person': {\n const result = await api.getPerson((args as { id: string }).id);\n return jsonResult(formatPerson(result.data));\n }\n\n case 'productive_get_current_user': {\n if (credentials.userId) {\n const result = await api.getPerson(credentials.userId);\n return jsonResult(formatPerson(result.data));\n }\n return jsonResult({\n message: 'User ID not configured. Set userId in credentials to use this tool.',\n organizationId: credentials.organizationId,\n });\n }\n\n default:\n return errorResult(`Unknown tool: ${name}`);\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return errorResult(message);\n }\n}\n"],"names":["cliFormatTimeEntry","cliFormatProject","cliFormatTask","cliFormatPerson","cliFormatService","cliFormatListResponse"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAOL,KAAK,GAAG,EACT,MAAM,IAAI,CAAC;AAMZ;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,GAAE,MAAM,GAAG,MAAM,GAAG,IAAW;;;;;;;EAM5F;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,GAAE,MAAM,GAAG,MAAM,GAAG,IAAW;;;;EAMhF;AAED;;GAEG;AACH,wBAAgB,gBAAgB;;;;;;;;;EAW/B;AAED;;GAEG;AACH,wBAAgB,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAE9B;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAyHnC"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAOL,KAAK,GAAG,EACT,MAAM,IAAI,CAAC;AAcZ;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,GAAE,MAAM,GAAG,MAAM,GAAG,IAAW;;;;;;;EAM5F;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,GAAE,MAAM,GAAG,MAAM,GAAG,IAAW;;;;EAMhF;AAED;;GAEG;AACH,wBAAgB,gBAAgB;;;;;;;;;EAW/B;AAED;;GAEG;AACH,wBAAgB,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAE9B;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAgInC"}
package/dist/http.js CHANGED
@@ -2,6 +2,8 @@ import { createApp, createRouter, defineEventHandler, getHeader, setResponseHead
2
2
  import { TOOLS } from "./tools.js";
3
3
  import { executeToolWithCredentials } from "./handlers.js";
4
4
  import { parseAuthHeader } from "./auth.js";
5
+ import { V as VERSION } from "./version-uWLfG4Z0.js";
6
+ import { oauthMetadataHandler, registerHandler, authorizeGetHandler, authorizePostHandler, tokenHandler } from "./oauth.js";
5
7
  function jsonRpcError(code, message, id = null) {
6
8
  return {
7
9
  jsonrpc: "2.0",
@@ -21,7 +23,7 @@ function handleInitialize() {
21
23
  protocolVersion: "2024-11-05",
22
24
  serverInfo: {
23
25
  name: "productive-mcp",
24
- version: "0.1.0"
26
+ version: VERSION
25
27
  },
26
28
  capabilities: {
27
29
  tools: {}
@@ -34,10 +36,15 @@ function handleToolsList() {
34
36
  function createHttpApp() {
35
37
  const app = createApp();
36
38
  const router = createRouter();
39
+ router.get("/.well-known/oauth-authorization-server", oauthMetadataHandler);
40
+ router.post("/register", registerHandler);
41
+ router.get("/authorize", authorizeGetHandler);
42
+ router.post("/authorize", authorizePostHandler);
43
+ router.post("/token", tokenHandler);
37
44
  router.get(
38
45
  "/",
39
46
  defineEventHandler(() => {
40
- return { status: "ok", service: "productive-mcp", version: "0.1.0" };
47
+ return { status: "ok", service: "productive-mcp", version: VERSION };
41
48
  })
42
49
  );
43
50
  router.get(
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 { TOOLS } from './tools.js';\nimport { executeToolWithCredentials } from './handlers.js';\nimport { parseAuthHeader } from './auth.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: '0.1.0',\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 // Health check endpoint\n router.get(\n '/',\n defineEventHandler(() => {\n return { status: 'ok', service: 'productive-mcp', version: '0.1.0' };\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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"}
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 { TOOLS } from './tools.js';\nimport { executeToolWithCredentials } from './handlers.js';\nimport { parseAuthHeader } from './auth.js';\nimport { VERSION } from './version.js';\nimport {\n oauthMetadataHandler,\n registerHandler,\n authorizeGetHandler,\n authorizePostHandler,\n tokenHandler,\n} from './oauth.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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAgBnE;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CA6C1C;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKtD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAiBnE;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CA6C1C;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKtD"}
package/dist/index.js CHANGED
@@ -3,11 +3,12 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { getAvailableTools, getAvailablePrompts, handlePrompt, handleToolCall } from "./stdio.js";
6
+ import { V as VERSION } from "./version-uWLfG4Z0.js";
6
7
  function createStdioServer() {
7
8
  const server = new Server(
8
9
  {
9
10
  name: "productive-mcp",
10
- version: "0.1.0"
11
+ version: VERSION
11
12
  },
12
13
  {
13
14
  capabilities: {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * Productive MCP Server - Stdio Transport\n *\n * This is the local execution mode using stdio transport.\n * For remote HTTP deployment, use server.ts instead.\n *\n * Usage:\n * npx @studiometa/productive-mcp\n *\n * Or in Claude Desktop config:\n * {\n * \"mcpServers\": {\n * \"productive\": {\n * \"command\": \"npx\",\n * \"args\": [\"@studiometa/productive-mcp\"]\n * }\n * }\n * }\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n ListPromptsRequestSchema,\n GetPromptRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js';\n\nimport {\n getAvailableTools,\n getAvailablePrompts,\n handleToolCall,\n handlePrompt,\n} from './stdio.js';\n\n/**\n * Create and configure the MCP server\n */\nexport function createStdioServer(): Server {\n const server = new Server(\n {\n name: 'productive-mcp',\n version: '0.1.0',\n },\n {\n capabilities: {\n tools: {},\n prompts: {},\n },\n }\n );\n\n // List available tools (including stdio-only configuration tools)\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return { tools: getAvailableTools() };\n });\n\n // List available prompts\n server.setRequestHandler(ListPromptsRequestSchema, async () => {\n return { prompts: getAvailablePrompts() };\n });\n\n // Get prompt\n server.setRequestHandler(GetPromptRequestSchema, async (request) => {\n return handlePrompt(request.params.name);\n });\n\n // Handle tool calls\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n try {\n return await handleToolCall(name, (args as Record<string, unknown>) || {});\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n content: [{ type: 'text', text: `Error: ${message}` }],\n isError: true,\n };\n }\n });\n\n return server;\n}\n\n/**\n * Start the stdio server\n */\nexport async function startStdioServer(): Promise<void> {\n const server = createStdioServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error('Productive MCP server running on stdio');\n}\n\n// Start server when run directly\nconst isMainModule = import.meta.url === `file://${process.argv[1]}` ||\n process.argv[1]?.endsWith('/productive-mcp') ||\n process.argv[1]?.endsWith('\\\\productive-mcp');\n\nif (isMainModule) {\n startStdioServer().catch((error) => {\n console.error('Fatal error:', error);\n process.exit(1);\n });\n}\n"],"names":[],"mappings":";;;;;AAyCO,SAAS,oBAA4B;AAC1C,QAAM,SAAS,IAAI;AAAA,IACjB;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,IAAA;AAAA,IAEX;AAAA,MACE,cAAc;AAAA,QACZ,OAAO,CAAA;AAAA,QACP,SAAS,CAAA;AAAA,MAAC;AAAA,IACZ;AAAA,EACF;AAIF,SAAO,kBAAkB,wBAAwB,YAAY;AAC3D,WAAO,EAAE,OAAO,oBAAkB;AAAA,EACpC,CAAC;AAGD,SAAO,kBAAkB,0BAA0B,YAAY;AAC7D,WAAO,EAAE,SAAS,sBAAoB;AAAA,EACxC,CAAC;AAGD,SAAO,kBAAkB,wBAAwB,OAAO,YAAY;AAClE,WAAO,aAAa,QAAQ,OAAO,IAAI;AAAA,EACzC,CAAC;AAGD,SAAO,kBAAkB,uBAAuB,OAAO,YAAY;AACjE,UAAM,EAAE,MAAM,WAAW,KAAA,IAAS,QAAQ;AAE1C,QAAI;AACF,aAAO,MAAM,eAAe,MAAO,QAAoC,CAAA,CAAE;AAAA,IAC3E,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,UAAU,OAAO,IAAI;AAAA,QACrD,SAAS;AAAA,MAAA;AAAA,IAEb;AAAA,EACF,CAAC;AAED,SAAO;AACT;AAKA,eAAsB,mBAAkC;AACtD,QAAM,SAAS,kBAAA;AACf,QAAM,YAAY,IAAI,qBAAA;AACtB,QAAM,OAAO,QAAQ,SAAS;AAC9B,UAAQ,MAAM,wCAAwC;AACxD;AAGA,MAAM,eAAe,YAAY,QAAQ,UAAU,QAAQ,KAAK,CAAC,CAAC,MAChE,QAAQ,KAAK,CAAC,GAAG,SAAS,iBAAiB,KAC3C,QAAQ,KAAK,CAAC,GAAG,SAAS,kBAAkB;AAE9C,IAAI,cAAc;AAChB,mBAAA,EAAmB,MAAM,CAAC,UAAU;AAClC,YAAQ,MAAM,gBAAgB,KAAK;AACnC,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * Productive MCP Server - Stdio Transport\n *\n * This is the local execution mode using stdio transport.\n * For remote HTTP deployment, use server.ts instead.\n *\n * Usage:\n * npx @studiometa/productive-mcp\n *\n * Or in Claude Desktop config:\n * {\n * \"mcpServers\": {\n * \"productive\": {\n * \"command\": \"npx\",\n * \"args\": [\"@studiometa/productive-mcp\"]\n * }\n * }\n * }\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n ListPromptsRequestSchema,\n GetPromptRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js';\n\nimport {\n getAvailableTools,\n getAvailablePrompts,\n handleToolCall,\n handlePrompt,\n} from './stdio.js';\nimport { VERSION } from './version.js';\n\n/**\n * Create and configure the MCP server\n */\nexport function createStdioServer(): Server {\n const server = new Server(\n {\n name: 'productive-mcp',\n version: VERSION,\n },\n {\n capabilities: {\n tools: {},\n prompts: {},\n },\n }\n );\n\n // List available tools (including stdio-only configuration tools)\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return { tools: getAvailableTools() };\n });\n\n // List available prompts\n server.setRequestHandler(ListPromptsRequestSchema, async () => {\n return { prompts: getAvailablePrompts() };\n });\n\n // Get prompt\n server.setRequestHandler(GetPromptRequestSchema, async (request) => {\n return handlePrompt(request.params.name);\n });\n\n // Handle tool calls\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n try {\n return await handleToolCall(name, (args as Record<string, unknown>) || {});\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n content: [{ type: 'text', text: `Error: ${message}` }],\n isError: true,\n };\n }\n });\n\n return server;\n}\n\n/**\n * Start the stdio server\n */\nexport async function startStdioServer(): Promise<void> {\n const server = createStdioServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error('Productive MCP server running on stdio');\n}\n\n// Start server when run directly\nconst isMainModule = import.meta.url === `file://${process.argv[1]}` ||\n process.argv[1]?.endsWith('/productive-mcp') ||\n process.argv[1]?.endsWith('\\\\productive-mcp');\n\nif (isMainModule) {\n startStdioServer().catch((error) => {\n console.error('Fatal error:', error);\n process.exit(1);\n });\n}\n"],"names":[],"mappings":";;;;;;AA0CO,SAAS,oBAA4B;AAC1C,QAAM,SAAS,IAAI;AAAA,IACjB;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,IAAA;AAAA,IAEX;AAAA,MACE,cAAc;AAAA,QACZ,OAAO,CAAA;AAAA,QACP,SAAS,CAAA;AAAA,MAAC;AAAA,IACZ;AAAA,EACF;AAIF,SAAO,kBAAkB,wBAAwB,YAAY;AAC3D,WAAO,EAAE,OAAO,oBAAkB;AAAA,EACpC,CAAC;AAGD,SAAO,kBAAkB,0BAA0B,YAAY;AAC7D,WAAO,EAAE,SAAS,sBAAoB;AAAA,EACxC,CAAC;AAGD,SAAO,kBAAkB,wBAAwB,OAAO,YAAY;AAClE,WAAO,aAAa,QAAQ,OAAO,IAAI;AAAA,EACzC,CAAC;AAGD,SAAO,kBAAkB,uBAAuB,OAAO,YAAY;AACjE,UAAM,EAAE,MAAM,WAAW,KAAA,IAAS,QAAQ;AAE1C,QAAI;AACF,aAAO,MAAM,eAAe,MAAO,QAAoC,CAAA,CAAE;AAAA,IAC3E,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,UAAU,OAAO,IAAI;AAAA,QACrD,SAAS;AAAA,MAAA;AAAA,IAEb;AAAA,EACF,CAAC;AAED,SAAO;AACT;AAKA,eAAsB,mBAAkC;AACtD,QAAM,SAAS,kBAAA;AACf,QAAM,YAAY,IAAI,qBAAA;AACtB,QAAM,OAAO,QAAQ,SAAS;AAC9B,UAAQ,MAAM,wCAAwC;AACxD;AAGA,MAAM,eAAe,YAAY,QAAQ,UAAU,QAAQ,KAAK,CAAC,CAAC,MAChE,QAAQ,KAAK,CAAC,GAAG,SAAS,iBAAiB,KAC3C,QAAQ,KAAK,CAAC,GAAG,SAAS,kBAAkB;AAE9C,IAAI,cAAc;AAChB,mBAAA,EAAmB,MAAM,CAAC,UAAU;AAClC,YAAQ,MAAM,gBAAgB,KAAK;AACnC,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * OAuth 2.0 endpoints for Claude Desktop integration
3
+ *
4
+ * Implements OAuth 2.1 with PKCE as specified in the MCP authorization spec.
5
+ * Uses stateless encrypted tokens - no server-side storage required.
6
+ *
7
+ * Spec: https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization
8
+ *
9
+ * Flow:
10
+ * 1. Claude redirects user to /authorize with OAuth params (including PKCE)
11
+ * 2. User enters Productive credentials in login form
12
+ * 3. Server encrypts credentials + PKCE challenge into authorization code
13
+ * 4. Redirects back to Claude with the code
14
+ * 5. Claude exchanges code for access token via /token (with code_verifier)
15
+ * 6. Server validates PKCE and returns access token
16
+ */
17
+ /**
18
+ * OAuth metadata for discovery (RFC 8414)
19
+ * GET /.well-known/oauth-authorization-server
20
+ *
21
+ * MCP clients MUST check this endpoint first for server capabilities.
22
+ */
23
+ export declare const oauthMetadataHandler: import("h3").EventHandler<import("h3").EventHandlerRequest, {
24
+ issuer: string;
25
+ authorization_endpoint: string;
26
+ token_endpoint: string;
27
+ response_types_supported: string[];
28
+ grant_types_supported: string[];
29
+ code_challenge_methods_supported: string[];
30
+ token_endpoint_auth_methods_supported: string[];
31
+ registration_endpoint: string;
32
+ scopes_supported: string[];
33
+ service_documentation: string;
34
+ }>;
35
+ /**
36
+ * Dynamic Client Registration endpoint (RFC 7591)
37
+ * POST /register
38
+ *
39
+ * MCP servers SHOULD support DCR to allow clients to register automatically.
40
+ * Since we use stateless tokens, we accept any registration and return
41
+ * a generated client_id.
42
+ */
43
+ export declare const registerHandler: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
44
+ error: string;
45
+ error_description: string;
46
+ client_id?: undefined;
47
+ client_name?: undefined;
48
+ redirect_uris?: undefined;
49
+ token_endpoint_auth_method?: undefined;
50
+ grant_types?: undefined;
51
+ response_types?: undefined;
52
+ } | {
53
+ client_id: string;
54
+ client_name: string;
55
+ redirect_uris: string[];
56
+ token_endpoint_auth_method: string;
57
+ grant_types: string[];
58
+ response_types: string[];
59
+ error?: undefined;
60
+ error_description?: undefined;
61
+ }>>;
62
+ /**
63
+ * Authorization endpoint - shows login form
64
+ * GET /authorize
65
+ */
66
+ export declare const authorizeGetHandler: import("h3").EventHandler<import("h3").EventHandlerRequest, string | Promise<void>>;
67
+ /**
68
+ * Authorization endpoint - process login
69
+ * POST /authorize
70
+ */
71
+ export declare const authorizePostHandler: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | void>>;
72
+ /**
73
+ * Token endpoint - exchange code for access token
74
+ * POST /token
75
+ *
76
+ * Supports:
77
+ * - authorization_code grant (with PKCE validation)
78
+ * - refresh_token grant
79
+ */
80
+ export declare const tokenHandler: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
81
+ error: string;
82
+ error_description: string;
83
+ access_token?: undefined;
84
+ token_type?: undefined;
85
+ expires_in?: undefined;
86
+ refresh_token?: undefined;
87
+ } | {
88
+ access_token: string;
89
+ token_type: string;
90
+ expires_in: number;
91
+ refresh_token: string;
92
+ error?: undefined;
93
+ error_description?: undefined;
94
+ }>>;
95
+ //# sourceMappingURL=oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAcH;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;EAyB/B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;GAoC1B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,qFA+C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,qFAgE/B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;GA+FvB,CAAC"}
package/dist/oauth.js ADDED
@@ -0,0 +1,492 @@
1
+ import { createHash } from "node:crypto";
2
+ import { defineEventHandler, setResponseHeader, readBody, getQuery, sendRedirect } from "h3";
3
+ import { createAuthCode, decodeAuthCode } from "./crypto.js";
4
+ import { createAuthToken } from "./auth.js";
5
+ const oauthMetadataHandler = defineEventHandler((event) => {
6
+ const host = event.node.req.headers.host || "localhost:3000";
7
+ const protocol = event.node.req.headers["x-forwarded-proto"] || "http";
8
+ const baseUrl = `${protocol}://${host}`;
9
+ setResponseHeader(event, "Content-Type", "application/json");
10
+ setResponseHeader(event, "Cache-Control", "public, max-age=3600");
11
+ return {
12
+ // Required fields per RFC 8414
13
+ issuer: baseUrl,
14
+ authorization_endpoint: `${baseUrl}/authorize`,
15
+ token_endpoint: `${baseUrl}/token`,
16
+ response_types_supported: ["code"],
17
+ // OAuth 2.1 / MCP requirements
18
+ grant_types_supported: ["authorization_code", "refresh_token"],
19
+ code_challenge_methods_supported: ["S256"],
20
+ token_endpoint_auth_methods_supported: ["none"],
21
+ // Public client
22
+ // Optional but useful
23
+ registration_endpoint: `${baseUrl}/register`,
24
+ scopes_supported: ["productive"],
25
+ service_documentation: "https://github.com/studiometa/productive-tools"
26
+ };
27
+ });
28
+ const registerHandler = defineEventHandler(async (event) => {
29
+ setResponseHeader(event, "Content-Type", "application/json");
30
+ let body;
31
+ try {
32
+ body = await readBody(event);
33
+ } catch {
34
+ event.node.res.statusCode = 400;
35
+ return {
36
+ error: "invalid_request",
37
+ error_description: "Invalid JSON body"
38
+ };
39
+ }
40
+ const clientName = body.client_name || "MCP Client";
41
+ const redirectUris = body.redirect_uris || [];
42
+ const clientId = Buffer.from(
43
+ JSON.stringify({
44
+ name: clientName,
45
+ ts: Date.now()
46
+ })
47
+ ).toString("base64url");
48
+ event.node.res.statusCode = 201;
49
+ return {
50
+ client_id: clientId,
51
+ client_name: clientName,
52
+ redirect_uris: redirectUris,
53
+ token_endpoint_auth_method: "none",
54
+ grant_types: ["authorization_code", "refresh_token"],
55
+ response_types: ["code"]
56
+ };
57
+ });
58
+ const authorizeGetHandler = defineEventHandler((event) => {
59
+ const query = getQuery(event);
60
+ query.client_id;
61
+ const redirectUri = query.redirect_uri;
62
+ const state = query.state;
63
+ const codeChallenge = query.code_challenge;
64
+ const codeChallengeMethod = query.code_challenge_method;
65
+ query.scope;
66
+ if (!redirectUri) {
67
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
68
+ event.node.res.statusCode = 400;
69
+ return renderErrorPage("Missing required parameter: redirect_uri");
70
+ }
71
+ if (!codeChallenge) {
72
+ const errorUrl = new URL(redirectUri);
73
+ errorUrl.searchParams.set("error", "invalid_request");
74
+ errorUrl.searchParams.set("error_description", "code_challenge is required");
75
+ if (state) errorUrl.searchParams.set("state", state);
76
+ return sendRedirect(event, errorUrl.toString());
77
+ }
78
+ if (codeChallengeMethod && codeChallengeMethod !== "S256") {
79
+ const errorUrl = new URL(redirectUri);
80
+ errorUrl.searchParams.set("error", "invalid_request");
81
+ errorUrl.searchParams.set("error_description", "Only S256 code_challenge_method is supported");
82
+ if (state) errorUrl.searchParams.set("state", state);
83
+ return sendRedirect(event, errorUrl.toString());
84
+ }
85
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
86
+ return renderLoginForm({
87
+ redirectUri,
88
+ state,
89
+ codeChallenge,
90
+ codeChallengeMethod: codeChallengeMethod || "S256"
91
+ });
92
+ });
93
+ const authorizePostHandler = defineEventHandler(async (event) => {
94
+ const body = await readBody(event);
95
+ const {
96
+ orgId,
97
+ apiToken,
98
+ userId,
99
+ redirectUri,
100
+ state,
101
+ codeChallenge,
102
+ codeChallengeMethod
103
+ } = body;
104
+ if (!redirectUri) {
105
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
106
+ event.node.res.statusCode = 400;
107
+ return renderErrorPage("Missing redirect_uri parameter");
108
+ }
109
+ try {
110
+ const uri = new URL(redirectUri);
111
+ const isLocalhost = uri.hostname === "localhost" || uri.hostname === "127.0.0.1";
112
+ const isHttps = uri.protocol === "https:";
113
+ if (!isLocalhost && !isHttps) {
114
+ event.node.res.statusCode = 400;
115
+ return renderErrorPage("redirect_uri must be HTTPS or localhost");
116
+ }
117
+ } catch {
118
+ event.node.res.statusCode = 400;
119
+ return renderErrorPage("Invalid redirect_uri format");
120
+ }
121
+ if (!orgId || !apiToken) {
122
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
123
+ return renderLoginForm({
124
+ redirectUri,
125
+ state,
126
+ codeChallenge,
127
+ codeChallengeMethod,
128
+ error: "Organization ID and API Token are required"
129
+ });
130
+ }
131
+ const code = createAuthCode({
132
+ orgId,
133
+ apiToken,
134
+ userId: userId || void 0,
135
+ codeChallenge,
136
+ codeChallengeMethod: codeChallengeMethod || "S256"
137
+ });
138
+ const redirectUrl = new URL(redirectUri);
139
+ redirectUrl.searchParams.set("code", code);
140
+ if (state) {
141
+ redirectUrl.searchParams.set("state", state);
142
+ }
143
+ return sendRedirect(event, redirectUrl.toString());
144
+ });
145
+ const tokenHandler = defineEventHandler(async (event) => {
146
+ setResponseHeader(event, "Content-Type", "application/json");
147
+ let body;
148
+ const contentType = event.node.req.headers["content-type"] || "";
149
+ if (contentType.includes("application/x-www-form-urlencoded")) {
150
+ const rawBody = await readBody(event);
151
+ if (typeof rawBody === "string") {
152
+ body = Object.fromEntries(new URLSearchParams(rawBody));
153
+ } else {
154
+ body = rawBody;
155
+ }
156
+ } else {
157
+ body = await readBody(event);
158
+ }
159
+ const { grant_type, code, code_verifier, refresh_token } = body;
160
+ if (grant_type === "refresh_token") {
161
+ return handleRefreshToken(event, refresh_token);
162
+ }
163
+ if (grant_type !== "authorization_code") {
164
+ event.node.res.statusCode = 400;
165
+ return {
166
+ error: "unsupported_grant_type",
167
+ error_description: "Supported grant types: authorization_code, refresh_token"
168
+ };
169
+ }
170
+ if (!code) {
171
+ event.node.res.statusCode = 400;
172
+ return {
173
+ error: "invalid_request",
174
+ error_description: "Missing authorization code"
175
+ };
176
+ }
177
+ if (!code_verifier) {
178
+ event.node.res.statusCode = 400;
179
+ return {
180
+ error: "invalid_request",
181
+ error_description: "Missing code_verifier (PKCE required)"
182
+ };
183
+ }
184
+ try {
185
+ const payload = decodeAuthCode(code);
186
+ if (payload.codeChallenge) {
187
+ const expectedChallenge = createS256Challenge(code_verifier);
188
+ if (expectedChallenge !== payload.codeChallenge) {
189
+ event.node.res.statusCode = 400;
190
+ return {
191
+ error: "invalid_grant",
192
+ error_description: "Invalid code_verifier"
193
+ };
194
+ }
195
+ }
196
+ const accessToken = createAuthToken({
197
+ organizationId: payload.orgId,
198
+ apiToken: payload.apiToken,
199
+ userId: payload.userId
200
+ });
201
+ const refreshToken = createAuthCode(
202
+ {
203
+ orgId: payload.orgId,
204
+ apiToken: payload.apiToken,
205
+ userId: payload.userId
206
+ },
207
+ 86400 * 30
208
+ // 30 days
209
+ );
210
+ return {
211
+ access_token: accessToken,
212
+ token_type: "Bearer",
213
+ expires_in: 3600,
214
+ // 1 hour (access tokens should be short-lived)
215
+ refresh_token: refreshToken
216
+ };
217
+ } catch (error) {
218
+ event.node.res.statusCode = 400;
219
+ return {
220
+ error: "invalid_grant",
221
+ error_description: error instanceof Error ? error.message : "Invalid authorization code"
222
+ };
223
+ }
224
+ });
225
+ function handleRefreshToken(event, refreshToken) {
226
+ if (!refreshToken) {
227
+ event.node.res.statusCode = 400;
228
+ return {
229
+ error: "invalid_request",
230
+ error_description: "Missing refresh_token"
231
+ };
232
+ }
233
+ try {
234
+ const payload = decodeAuthCode(refreshToken);
235
+ const accessToken = createAuthToken({
236
+ organizationId: payload.orgId,
237
+ apiToken: payload.apiToken,
238
+ userId: payload.userId
239
+ });
240
+ const newRefreshToken = createAuthCode(
241
+ {
242
+ orgId: payload.orgId,
243
+ apiToken: payload.apiToken,
244
+ userId: payload.userId
245
+ },
246
+ 86400 * 30
247
+ // 30 days
248
+ );
249
+ return {
250
+ access_token: accessToken,
251
+ token_type: "Bearer",
252
+ expires_in: 3600,
253
+ refresh_token: newRefreshToken
254
+ };
255
+ } catch (error) {
256
+ event.node.res.statusCode = 400;
257
+ return {
258
+ error: "invalid_grant",
259
+ error_description: error instanceof Error ? error.message : "Invalid refresh token"
260
+ };
261
+ }
262
+ }
263
+ function createS256Challenge(codeVerifier) {
264
+ return createHash("sha256").update(codeVerifier).digest("base64url");
265
+ }
266
+ function renderLoginForm(params) {
267
+ const { redirectUri, state, codeChallenge, codeChallengeMethod, error } = params;
268
+ return `<!DOCTYPE html>
269
+ <html lang="en">
270
+ <head>
271
+ <meta charset="UTF-8">
272
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
273
+ <title>Connect to Productive.io</title>
274
+ <style>
275
+ * {
276
+ box-sizing: border-box;
277
+ margin: 0;
278
+ padding: 0;
279
+ }
280
+ body {
281
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
282
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
283
+ min-height: 100vh;
284
+ display: flex;
285
+ align-items: center;
286
+ justify-content: center;
287
+ padding: 20px;
288
+ }
289
+ .container {
290
+ background: white;
291
+ border-radius: 16px;
292
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
293
+ padding: 40px;
294
+ width: 100%;
295
+ max-width: 420px;
296
+ }
297
+ .logo {
298
+ text-align: center;
299
+ margin-bottom: 24px;
300
+ }
301
+ .logo svg {
302
+ width: 48px;
303
+ height: 48px;
304
+ }
305
+ h1 {
306
+ text-align: center;
307
+ color: #1a1a2e;
308
+ font-size: 24px;
309
+ margin-bottom: 8px;
310
+ }
311
+ .subtitle {
312
+ text-align: center;
313
+ color: #666;
314
+ font-size: 14px;
315
+ margin-bottom: 32px;
316
+ }
317
+ .error {
318
+ background: #fee2e2;
319
+ border: 1px solid #fecaca;
320
+ color: #dc2626;
321
+ padding: 12px 16px;
322
+ border-radius: 8px;
323
+ margin-bottom: 24px;
324
+ font-size: 14px;
325
+ }
326
+ .form-group {
327
+ margin-bottom: 20px;
328
+ }
329
+ label {
330
+ display: block;
331
+ font-size: 14px;
332
+ font-weight: 500;
333
+ color: #374151;
334
+ margin-bottom: 6px;
335
+ }
336
+ input {
337
+ width: 100%;
338
+ padding: 12px 16px;
339
+ border: 1px solid #d1d5db;
340
+ border-radius: 8px;
341
+ font-size: 16px;
342
+ transition: border-color 0.2s, box-shadow 0.2s;
343
+ }
344
+ input:focus {
345
+ outline: none;
346
+ border-color: #667eea;
347
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
348
+ }
349
+ input::placeholder {
350
+ color: #9ca3af;
351
+ }
352
+ .help-text {
353
+ font-size: 12px;
354
+ color: #6b7280;
355
+ margin-top: 4px;
356
+ }
357
+ button {
358
+ width: 100%;
359
+ padding: 14px 24px;
360
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
361
+ color: white;
362
+ border: none;
363
+ border-radius: 8px;
364
+ font-size: 16px;
365
+ font-weight: 600;
366
+ cursor: pointer;
367
+ transition: transform 0.2s, box-shadow 0.2s;
368
+ }
369
+ button:hover {
370
+ transform: translateY(-1px);
371
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
372
+ }
373
+ button:active {
374
+ transform: translateY(0);
375
+ }
376
+ .footer {
377
+ text-align: center;
378
+ margin-top: 24px;
379
+ font-size: 12px;
380
+ color: #9ca3af;
381
+ }
382
+ .footer a {
383
+ color: #667eea;
384
+ text-decoration: none;
385
+ }
386
+ .footer a:hover {
387
+ text-decoration: underline;
388
+ }
389
+ </style>
390
+ </head>
391
+ <body>
392
+ <div class="container">
393
+ <div class="logo">
394
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
395
+ <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="#667eea" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
396
+ <path d="M2 17L12 22L22 17" stroke="#764ba2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
397
+ <path d="M2 12L12 17L22 12" stroke="#667eea" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
398
+ </svg>
399
+ </div>
400
+ <h1>Connect to Productive.io</h1>
401
+ <p class="subtitle">Enter your Productive.io credentials to connect with Claude</p>
402
+
403
+ ${error ? `<div class="error">${escapeHtml(error)}</div>` : ""}
404
+
405
+ <form method="POST" action="/authorize">
406
+ <input type="hidden" name="redirectUri" value="${escapeHtml(redirectUri || "")}">
407
+ <input type="hidden" name="state" value="${escapeHtml(state || "")}">
408
+ <input type="hidden" name="codeChallenge" value="${escapeHtml(codeChallenge || "")}">
409
+ <input type="hidden" name="codeChallengeMethod" value="${escapeHtml(codeChallengeMethod || "S256")}">
410
+
411
+ <div class="form-group">
412
+ <label for="orgId">Organization ID *</label>
413
+ <input type="text" id="orgId" name="orgId" required placeholder="e.g., 12345">
414
+ <p class="help-text">Found in Settings → API integrations</p>
415
+ </div>
416
+
417
+ <div class="form-group">
418
+ <label for="apiToken">API Token *</label>
419
+ <input type="password" id="apiToken" name="apiToken" required placeholder="pk_...">
420
+ <p class="help-text">Generate at Settings → API integrations → Generate new token</p>
421
+ </div>
422
+
423
+ <div class="form-group">
424
+ <label for="userId">User ID (optional)</label>
425
+ <input type="text" id="userId" name="userId" placeholder="e.g., 67890">
426
+ <p class="help-text">Required for creating time entries. Found in your profile URL.</p>
427
+ </div>
428
+
429
+ <button type="submit">Connect to Productive</button>
430
+ </form>
431
+
432
+ <p class="footer">
433
+ Your credentials are encrypted and sent directly to Claude.<br>
434
+ <a href="https://developer.productive.io" target="_blank">Productive.io API Documentation</a>
435
+ </p>
436
+ </div>
437
+ </body>
438
+ </html>`;
439
+ }
440
+ function renderErrorPage(message) {
441
+ return `<!DOCTYPE html>
442
+ <html lang="en">
443
+ <head>
444
+ <meta charset="UTF-8">
445
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
446
+ <title>Error - Productive MCP</title>
447
+ <style>
448
+ body {
449
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
450
+ background: #f3f4f6;
451
+ min-height: 100vh;
452
+ display: flex;
453
+ align-items: center;
454
+ justify-content: center;
455
+ padding: 20px;
456
+ }
457
+ .container {
458
+ background: white;
459
+ border-radius: 16px;
460
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
461
+ padding: 40px;
462
+ text-align: center;
463
+ max-width: 400px;
464
+ }
465
+ h1 {
466
+ color: #dc2626;
467
+ margin-bottom: 16px;
468
+ }
469
+ p {
470
+ color: #6b7280;
471
+ }
472
+ </style>
473
+ </head>
474
+ <body>
475
+ <div class="container">
476
+ <h1>Error</h1>
477
+ <p>${escapeHtml(message)}</p>
478
+ </div>
479
+ </body>
480
+ </html>`;
481
+ }
482
+ function escapeHtml(str) {
483
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
484
+ }
485
+ export {
486
+ authorizeGetHandler,
487
+ authorizePostHandler,
488
+ oauthMetadataHandler,
489
+ registerHandler,
490
+ tokenHandler
491
+ };
492
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","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 { createHash } from 'node:crypto';\nimport {\n defineEventHandler,\n getQuery,\n readBody,\n sendRedirect,\n setResponseHeader,\n type H3Event,\n} from 'h3';\nimport { createAuthCode, decodeAuthCode } from './crypto.js';\nimport { createAuthToken } from './auth.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 = defineEventHandler((event: H3Event) => {\n const host = event.node.req.headers.host || 'localhost:3000';\n const protocol = event.node.req.headers['x-forwarded-proto'] || 'http';\n const baseUrl = `${protocol}://${host}`;\n\n setResponseHeader(event, 'Content-Type', 'application/json');\n setResponseHeader(event, '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 = defineEventHandler(async (event: H3Event) => {\n setResponseHeader(event, 'Content-Type', 'application/json');\n\n let body: Record<string, unknown>;\n try {\n body = await readBody(event);\n } catch {\n event.node.res.statusCode = 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.node.res.statusCode = 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 = defineEventHandler((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 setResponseHeader(event, 'Content-Type', 'text/html; charset=utf-8');\n event.node.res.statusCode = 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 sendRedirect(event, 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 sendRedirect(event, errorUrl.toString());\n }\n\n setResponseHeader(event, '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 = defineEventHandler(async (event: H3Event) => {\n const body = await readBody(event);\n\n const {\n orgId,\n apiToken,\n userId,\n redirectUri,\n state,\n codeChallenge,\n codeChallengeMethod,\n } = body;\n\n // Validate redirect URI first (security requirement)\n if (!redirectUri) {\n setResponseHeader(event, 'Content-Type', 'text/html; charset=utf-8');\n event.node.res.statusCode = 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.node.res.statusCode = 400;\n return renderErrorPage('redirect_uri must be HTTPS or localhost');\n }\n } catch {\n event.node.res.statusCode = 400;\n return renderErrorPage('Invalid redirect_uri format');\n }\n\n // Validate required credentials\n if (!orgId || !apiToken) {\n setResponseHeader(event, '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 Claude\n return sendRedirect(event, 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 = defineEventHandler(async (event: H3Event) => {\n setResponseHeader(event, 'Content-Type', 'application/json');\n\n let body: Record<string, string>;\n const contentType = event.node.req.headers['content-type'] || '';\n\n if (contentType.includes('application/x-www-form-urlencoded')) {\n const rawBody = await readBody(event);\n if (typeof rawBody === 'string') {\n body = Object.fromEntries(new URLSearchParams(rawBody));\n } else {\n body = rawBody;\n }\n } else {\n body = await readBody(event);\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.node.res.statusCode = 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.node.res.statusCode = 400;\n return {\n error: 'invalid_request',\n error_description: 'Missing authorization code',\n };\n }\n\n if (!code_verifier) {\n event.node.res.statusCode = 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.node.res.statusCode = 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.node.res.statusCode = 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.node.res.statusCode = 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.node.res.statusCode = 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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#039;');\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAOtD;;GAEG;AACH,wBAAgB,eAAe,CAC7B,IAAI,GAAE,MAAqB,EAC3B,IAAI,GAAE,MAAqB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAsBjB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAQtD;;GAEG;AACH,wBAAgB,eAAe,CAC7B,IAAI,GAAE,MAAqB,EAC3B,IAAI,GAAE,MAAqB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAmCjB"}
package/dist/server.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { createServer } from "node:http";
3
3
  import { toNodeListener } from "h3";
4
4
  import { createHttpApp } from "./http.js";
5
+ import { V as VERSION } from "./version-uWLfG4Z0.js";
5
6
  const DEFAULT_PORT = 3e3;
6
7
  const DEFAULT_HOST = "0.0.0.0";
7
8
  function startHttpServer(port = DEFAULT_PORT, host = DEFAULT_HOST) {
@@ -10,18 +11,31 @@ function startHttpServer(port = DEFAULT_PORT, host = DEFAULT_HOST) {
10
11
  const server = createServer(toNodeListener(app));
11
12
  server.listen(port, host, () => {
12
13
  const displayHost = host === "0.0.0.0" ? "localhost" : host;
13
- console.log(`Productive MCP server running at http://${displayHost}:${port}`);
14
+ console.log(`Productive MCP server v${VERSION}`);
15
+ console.log(`Node.js ${process.version}`);
16
+ console.log("");
17
+ console.log(`Running at http://${displayHost}:${port}`);
14
18
  console.log("");
15
19
  console.log("Endpoints:");
16
20
  console.log(` POST http://${displayHost}:${port}/mcp - MCP JSON-RPC endpoint`);
17
21
  console.log(` GET http://${displayHost}:${port}/health - Health check`);
18
22
  console.log("");
23
+ console.log("OAuth 2.0 (MCP auth spec compliant):");
24
+ console.log(` GET http://${displayHost}:${port}/.well-known/oauth-authorization-server`);
25
+ console.log(` POST http://${displayHost}:${port}/register - Dynamic Client Registration`);
26
+ console.log(` GET http://${displayHost}:${port}/authorize - Authorization endpoint`);
27
+ console.log(` POST http://${displayHost}:${port}/token - Token endpoint`);
28
+ console.log("");
19
29
  console.log("Authentication:");
20
- console.log(" Pass Bearer token in Authorization header");
21
- console.log(" Token format: base64(organizationId:apiToken:userId)");
30
+ console.log(" Option 1: OAuth flow (Claude Desktop will handle this automatically)");
31
+ console.log(" Option 2: Bearer token in Authorization header");
32
+ console.log(" Token format: base64(organizationId:apiToken:userId)");
22
33
  console.log("");
23
- console.log("Generate token:");
24
- console.log(' echo -n "ORG_ID:API_TOKEN:USER_ID" | base64');
34
+ if (!process.env.OAUTH_SECRET) {
35
+ console.log("⚠️ WARNING: OAUTH_SECRET not set. Set it in production!");
36
+ console.log(' export OAUTH_SECRET="your-random-secret-here"');
37
+ console.log("");
38
+ }
25
39
  resolve(server);
26
40
  });
27
41
  });
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sources":["../src/server.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * Productive MCP Server - HTTP Transport\n *\n * This is the remote HTTP server mode for Claude Desktop custom connectors.\n * Credentials are passed via Bearer token in the Authorization header.\n *\n * Token format: base64(organizationId:apiToken) or base64(organizationId:apiToken:userId)\n *\n * Generate your token:\n * echo -n \"YOUR_ORG_ID:YOUR_API_TOKEN:YOUR_USER_ID\" | base64\n *\n * Usage:\n * productive-mcp-server\n * PORT=3000 productive-mcp-server\n *\n * Claude Desktop custom connector config:\n * Name: Productive\n * URL: https://productive.mcp.ikko.dev\n * (No OAuth needed - uses Bearer token)\n */\n\nimport { createServer, type Server } from 'node:http';\nimport { toNodeListener } from 'h3';\nimport { createHttpApp } from './http.js';\n\nconst DEFAULT_PORT = 3000;\nconst DEFAULT_HOST = '0.0.0.0';\n\n/**\n * Start the HTTP server\n */\nexport function startHttpServer(\n port: number = DEFAULT_PORT,\n host: string = DEFAULT_HOST\n): Promise<Server> {\n return new Promise((resolve) => {\n const app = createHttpApp();\n const server = createServer(toNodeListener(app));\n\n server.listen(port, host, () => {\n const displayHost = host === '0.0.0.0' ? 'localhost' : host;\n console.log(`Productive MCP server running at http://${displayHost}:${port}`);\n console.log('');\n console.log('Endpoints:');\n console.log(` POST http://${displayHost}:${port}/mcp - MCP JSON-RPC endpoint`);\n console.log(` GET http://${displayHost}:${port}/health - Health check`);\n console.log('');\n console.log('Authentication:');\n console.log(' Pass Bearer token in Authorization header');\n console.log(' Token format: base64(organizationId:apiToken:userId)');\n console.log('');\n console.log('Generate token:');\n console.log(' echo -n \"ORG_ID:API_TOKEN:USER_ID\" | base64');\n resolve(server);\n });\n });\n}\n\n// Start server when run directly\nconst isMainModule =\n import.meta.url === `file://${process.argv[1]}` ||\n process.argv[1]?.endsWith('/productive-mcp-server') ||\n process.argv[1]?.endsWith('\\\\productive-mcp-server');\n\nif (isMainModule) {\n const port = Number.parseInt(process.env.PORT || String(DEFAULT_PORT), 10);\n const host = process.env.HOST || DEFAULT_HOST;\n\n startHttpServer(port, host).catch((error) => {\n console.error('Fatal error:', error);\n process.exit(1);\n });\n}\n"],"names":[],"mappings":";;;;AA2BA,MAAM,eAAe;AACrB,MAAM,eAAe;AAKd,SAAS,gBACd,OAAe,cACf,OAAe,cACE;AACjB,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,MAAM,cAAA;AACZ,UAAM,SAAS,aAAa,eAAe,GAAG,CAAC;AAE/C,WAAO,OAAO,MAAM,MAAM,MAAM;AAC9B,YAAM,cAAc,SAAS,YAAY,cAAc;AACvD,cAAQ,IAAI,2CAA2C,WAAW,IAAI,IAAI,EAAE;AAC5E,cAAQ,IAAI,EAAE;AACd,cAAQ,IAAI,YAAY;AACxB,cAAQ,IAAI,iBAAiB,WAAW,IAAI,IAAI,8BAA8B;AAC9E,cAAQ,IAAI,iBAAiB,WAAW,IAAI,IAAI,wBAAwB;AACxE,cAAQ,IAAI,EAAE;AACd,cAAQ,IAAI,iBAAiB;AAC7B,cAAQ,IAAI,6CAA6C;AACzD,cAAQ,IAAI,wDAAwD;AACpE,cAAQ,IAAI,EAAE;AACd,cAAQ,IAAI,iBAAiB;AAC7B,cAAQ,IAAI,+CAA+C;AAC3D,cAAQ,MAAM;AAAA,IAChB,CAAC;AAAA,EACH,CAAC;AACH;AAGA,MAAM,eACJ,YAAY,QAAQ,UAAU,QAAQ,KAAK,CAAC,CAAC,MAC7C,QAAQ,KAAK,CAAC,GAAG,SAAS,wBAAwB,KAClD,QAAQ,KAAK,CAAC,GAAG,SAAS,yBAAyB;AAErD,IAAI,cAAc;AAChB,QAAM,OAAO,OAAO,SAAS,QAAQ,IAAI,QAAQ,OAAO,YAAY,GAAG,EAAE;AACzE,QAAM,OAAO,QAAQ,IAAI,QAAQ;AAEjC,kBAAgB,MAAM,IAAI,EAAE,MAAM,CAAC,UAAU;AAC3C,YAAQ,MAAM,gBAAgB,KAAK;AACnC,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;"}
1
+ {"version":3,"file":"server.js","sources":["../src/server.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * Productive MCP Server - HTTP Transport\n *\n * This is the remote HTTP server mode for Claude Desktop custom connectors.\n * Credentials are passed via Bearer token in the Authorization header.\n *\n * Token format: base64(organizationId:apiToken) or base64(organizationId:apiToken:userId)\n *\n * Generate your token:\n * echo -n \"YOUR_ORG_ID:YOUR_API_TOKEN:YOUR_USER_ID\" | base64\n *\n * Usage:\n * productive-mcp-server\n * PORT=3000 productive-mcp-server\n *\n * Claude Desktop custom connector config:\n * Name: Productive\n * URL: https://productive.mcp.ikko.dev\n * (No OAuth needed - uses Bearer token)\n */\n\nimport { createServer, type Server } from 'node:http';\nimport { toNodeListener } from 'h3';\nimport { createHttpApp } from './http.js';\nimport { VERSION } from './version.js';\n\nconst DEFAULT_PORT = 3000;\nconst DEFAULT_HOST = '0.0.0.0';\n\n/**\n * Start the HTTP server\n */\nexport function startHttpServer(\n port: number = DEFAULT_PORT,\n host: string = DEFAULT_HOST\n): Promise<Server> {\n return new Promise((resolve) => {\n const app = createHttpApp();\n const server = createServer(toNodeListener(app));\n\n server.listen(port, host, () => {\n const displayHost = host === '0.0.0.0' ? 'localhost' : host;\n console.log(`Productive MCP server v${VERSION}`);\n console.log(`Node.js ${process.version}`);\n console.log('');\n console.log(`Running at http://${displayHost}:${port}`);\n console.log('');\n console.log('Endpoints:');\n console.log(` POST http://${displayHost}:${port}/mcp - MCP JSON-RPC endpoint`);\n console.log(` GET http://${displayHost}:${port}/health - Health check`);\n console.log('');\n console.log('OAuth 2.0 (MCP auth spec compliant):');\n console.log(` GET http://${displayHost}:${port}/.well-known/oauth-authorization-server`);\n console.log(` POST http://${displayHost}:${port}/register - Dynamic Client Registration`);\n console.log(` GET http://${displayHost}:${port}/authorize - Authorization endpoint`);\n console.log(` POST http://${displayHost}:${port}/token - Token endpoint`);\n console.log('');\n console.log('Authentication:');\n console.log(' Option 1: OAuth flow (Claude Desktop will handle this automatically)');\n console.log(' Option 2: Bearer token in Authorization header');\n console.log(' Token format: base64(organizationId:apiToken:userId)');\n console.log('');\n if (!process.env.OAUTH_SECRET) {\n console.log('⚠️ WARNING: OAUTH_SECRET not set. Set it in production!');\n console.log(' export OAUTH_SECRET=\"your-random-secret-here\"');\n console.log('');\n }\n resolve(server);\n });\n });\n}\n\n// Start server when run directly\nconst isMainModule =\n import.meta.url === `file://${process.argv[1]}` ||\n process.argv[1]?.endsWith('/productive-mcp-server') ||\n process.argv[1]?.endsWith('\\\\productive-mcp-server');\n\nif (isMainModule) {\n const port = Number.parseInt(process.env.PORT || String(DEFAULT_PORT), 10);\n const host = process.env.HOST || DEFAULT_HOST;\n\n startHttpServer(port, host).catch((error) => {\n console.error('Fatal error:', error);\n process.exit(1);\n });\n}\n"],"names":[],"mappings":";;;;;AA4BA,MAAM,eAAe;AACrB,MAAM,eAAe;AAKd,SAAS,gBACd,OAAe,cACf,OAAe,cACE;AACjB,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,MAAM,cAAA;AACZ,UAAM,SAAS,aAAa,eAAe,GAAG,CAAC;AAE/C,WAAO,OAAO,MAAM,MAAM,MAAM;AAC9B,YAAM,cAAc,SAAS,YAAY,cAAc;AACvD,cAAQ,IAAI,0BAA0B,OAAO,EAAE;AAC/C,cAAQ,IAAI,WAAW,QAAQ,OAAO,EAAE;AACxC,cAAQ,IAAI,EAAE;AACd,cAAQ,IAAI,qBAAqB,WAAW,IAAI,IAAI,EAAE;AACtD,cAAQ,IAAI,EAAE;AACd,cAAQ,IAAI,YAAY;AACxB,cAAQ,IAAI,iBAAiB,WAAW,IAAI,IAAI,8BAA8B;AAC9E,cAAQ,IAAI,iBAAiB,WAAW,IAAI,IAAI,wBAAwB;AACxE,cAAQ,IAAI,EAAE;AACd,cAAQ,IAAI,sCAAsC;AAClD,cAAQ,IAAI,iBAAiB,WAAW,IAAI,IAAI,yCAAyC;AACzF,cAAQ,IAAI,iBAAiB,WAAW,IAAI,IAAI,yCAAyC;AACzF,cAAQ,IAAI,iBAAiB,WAAW,IAAI,IAAI,qCAAqC;AACrF,cAAQ,IAAI,iBAAiB,WAAW,IAAI,IAAI,yBAAyB;AACzE,cAAQ,IAAI,EAAE;AACd,cAAQ,IAAI,iBAAiB;AAC7B,cAAQ,IAAI,wEAAwE;AACpF,cAAQ,IAAI,kDAAkD;AAC9D,cAAQ,IAAI,kEAAkE;AAC9E,cAAQ,IAAI,EAAE;AACd,UAAI,CAAC,QAAQ,IAAI,cAAc;AAC7B,gBAAQ,IAAI,0DAA0D;AACtE,gBAAQ,IAAI,kDAAkD;AAC9D,gBAAQ,IAAI,EAAE;AAAA,MAChB;AACA,cAAQ,MAAM;AAAA,IAChB,CAAC;AAAA,EACH,CAAC;AACH;AAGA,MAAM,eACJ,YAAY,QAAQ,UAAU,QAAQ,KAAK,CAAC,CAAC,MAC7C,QAAQ,KAAK,CAAC,GAAG,SAAS,wBAAwB,KAClD,QAAQ,KAAK,CAAC,GAAG,SAAS,yBAAyB;AAErD,IAAI,cAAc;AAChB,QAAM,OAAO,OAAO,SAAS,QAAQ,IAAI,QAAQ,OAAO,YAAY,GAAG,EAAE;AACzE,QAAM,OAAO,QAAQ,IAAI,QAAQ;AAEjC,kBAAgB,MAAM,IAAI,EAAE,MAAM,CAAC,UAAU;AAC3C,YAAQ,MAAM,gBAAgB,KAAK;AACnC,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;"}
@@ -0,0 +1,5 @@
1
+ const VERSION = "0.4.6";
2
+ export {
3
+ VERSION as V
4
+ };
5
+ //# sourceMappingURL=version-uWLfG4Z0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-uWLfG4Z0.js","sources":["../src/version.ts"],"sourcesContent":["/**\n * Package version - injected from package.json at build time\n */\ndeclare const __VERSION__: string;\nexport const VERSION = __VERSION__;\n"],"names":[],"mappings":"AAIO,MAAM,UAAU;"}
@@ -0,0 +1,2 @@
1
+ export declare const VERSION: string;
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,OAAO,QAAc,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studiometa/productive-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.6",
4
4
  "description": "MCP server for Productive.io API - Model Context Protocol integration for Claude Desktop",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -25,6 +25,14 @@
25
25
  "import": "./dist/auth.js",
26
26
  "types": "./dist/auth.d.ts"
27
27
  },
28
+ "./crypto": {
29
+ "import": "./dist/crypto.js",
30
+ "types": "./dist/crypto.d.ts"
31
+ },
32
+ "./oauth": {
33
+ "import": "./dist/oauth.js",
34
+ "types": "./dist/oauth.d.ts"
35
+ },
28
36
  "./tools": {
29
37
  "import": "./dist/tools.js",
30
38
  "types": "./dist/tools.d.ts"
@@ -65,7 +73,7 @@
65
73
  "license": "MIT",
66
74
  "repository": {
67
75
  "type": "git",
68
- "url": "https://github.com/studiometa/productive-cli",
76
+ "url": "https://github.com/studiometa/productive-tools",
69
77
  "directory": "packages/productive-mcp"
70
78
  },
71
79
  "engines": {