@studiometa/productive-mcp 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/Dockerfile +7 -0
  2. package/README.md +51 -43
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js.map +1 -1
  5. package/dist/crypto.js +1 -1
  6. package/dist/crypto.js.map +1 -1
  7. package/dist/formatters.d.ts +20 -0
  8. package/dist/formatters.d.ts.map +1 -1
  9. package/dist/handlers/bookings.d.ts +6 -0
  10. package/dist/handlers/bookings.d.ts.map +1 -0
  11. package/dist/handlers/comments.d.ts +6 -0
  12. package/dist/handlers/comments.d.ts.map +1 -0
  13. package/dist/handlers/companies.d.ts +6 -0
  14. package/dist/handlers/companies.d.ts.map +1 -0
  15. package/dist/handlers/deals.d.ts +6 -0
  16. package/dist/handlers/deals.d.ts.map +1 -0
  17. package/dist/handlers/index.d.ts +15 -0
  18. package/dist/handlers/index.d.ts.map +1 -0
  19. package/dist/handlers/people.d.ts +7 -0
  20. package/dist/handlers/people.d.ts.map +1 -0
  21. package/dist/handlers/projects.d.ts +6 -0
  22. package/dist/handlers/projects.d.ts.map +1 -0
  23. package/dist/handlers/services.d.ts +6 -0
  24. package/dist/handlers/services.d.ts.map +1 -0
  25. package/dist/handlers/tasks.d.ts +6 -0
  26. package/dist/handlers/tasks.d.ts.map +1 -0
  27. package/dist/handlers/time.d.ts +6 -0
  28. package/dist/handlers/time.d.ts.map +1 -0
  29. package/dist/handlers/timers.d.ts +6 -0
  30. package/dist/handlers/timers.d.ts.map +1 -0
  31. package/dist/handlers/types.d.ts +78 -0
  32. package/dist/handlers/types.d.ts.map +1 -0
  33. package/dist/handlers/utils.d.ts +17 -0
  34. package/dist/handlers/utils.d.ts.map +1 -0
  35. package/dist/handlers.d.ts +3 -10
  36. package/dist/handlers.d.ts.map +1 -1
  37. package/dist/handlers.js +2 -233
  38. package/dist/handlers.js.map +1 -1
  39. package/dist/http.js +3 -3
  40. package/dist/http.js.map +1 -1
  41. package/dist/index-CmTDkz-y.js +480 -0
  42. package/dist/index-CmTDkz-y.js.map +1 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1 -1
  45. package/dist/index.js.map +1 -1
  46. package/dist/oauth.d.ts +1 -1
  47. package/dist/oauth.d.ts.map +1 -1
  48. package/dist/oauth.js +111 -12
  49. package/dist/oauth.js.map +1 -1
  50. package/dist/server.d.ts.map +1 -1
  51. package/dist/server.js +2 -2
  52. package/dist/server.js.map +1 -1
  53. package/dist/stdio.d.ts.map +1 -1
  54. package/dist/stdio.js +1 -1
  55. package/dist/stdio.js.map +1 -1
  56. package/dist/tools.d.ts.map +1 -1
  57. package/dist/tools.js +34 -5
  58. package/dist/tools.js.map +1 -1
  59. package/dist/version-BBHuTm1A.js +5 -0
  60. package/dist/{version-eQNCcjOb.js.map → version-BBHuTm1A.js.map} +1 -1
  61. package/package.json +46 -46
  62. package/dist/version-eQNCcjOb.js +0 -5
package/dist/oauth.js CHANGED
@@ -1,7 +1,7 @@
1
- import { createHash } from "node:crypto";
2
1
  import { defineEventHandler, setResponseHeader, readBody, getQuery, sendRedirect } from "h3";
3
- import { createAuthCode, decodeAuthCode } from "./crypto.js";
2
+ import { createHash } from "node:crypto";
4
3
  import { createAuthToken } from "./auth.js";
4
+ import { createAuthCode, decodeAuthCode } from "./crypto.js";
5
5
  const oauthMetadataHandler = defineEventHandler((event) => {
6
6
  const host = event.node.req.headers.host || "localhost:3000";
7
7
  const protocol = event.node.req.headers["x-forwarded-proto"] || "http";
@@ -92,15 +92,7 @@ const authorizeGetHandler = defineEventHandler((event) => {
92
92
  });
93
93
  const authorizePostHandler = defineEventHandler(async (event) => {
94
94
  const body = await readBody(event);
95
- const {
96
- orgId,
97
- apiToken,
98
- userId,
99
- redirectUri,
100
- state,
101
- codeChallenge,
102
- codeChallengeMethod
103
- } = body;
95
+ const { orgId, apiToken, userId, redirectUri, state, codeChallenge, codeChallengeMethod } = body;
104
96
  if (!redirectUri) {
105
97
  setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
106
98
  event.node.res.statusCode = 400;
@@ -140,7 +132,8 @@ const authorizePostHandler = defineEventHandler(async (event) => {
140
132
  if (state) {
141
133
  redirectUrl.searchParams.set("state", state);
142
134
  }
143
- return sendRedirect(event, redirectUrl.toString());
135
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
136
+ return renderSuccessPage(redirectUrl.toString());
144
137
  });
145
138
  const tokenHandler = defineEventHandler(async (event) => {
146
139
  setResponseHeader(event, "Content-Type", "application/json");
@@ -437,6 +430,112 @@ function renderLoginForm(params) {
437
430
  </body>
438
431
  </html>`;
439
432
  }
433
+ function renderSuccessPage(redirectUrl) {
434
+ return `<!DOCTYPE html>
435
+ <html lang="en">
436
+ <head>
437
+ <meta charset="UTF-8">
438
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
439
+ <meta http-equiv="refresh" content="2;url=${escapeHtml(redirectUrl)}">
440
+ <title>Connected - Productive MCP</title>
441
+ <style>
442
+ * {
443
+ box-sizing: border-box;
444
+ margin: 0;
445
+ padding: 0;
446
+ }
447
+ body {
448
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
449
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
450
+ min-height: 100vh;
451
+ display: flex;
452
+ align-items: center;
453
+ justify-content: center;
454
+ padding: 20px;
455
+ }
456
+ .container {
457
+ background: white;
458
+ border-radius: 16px;
459
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
460
+ padding: 40px;
461
+ width: 100%;
462
+ max-width: 420px;
463
+ text-align: center;
464
+ }
465
+ .success-icon {
466
+ width: 64px;
467
+ height: 64px;
468
+ margin: 0 auto 24px;
469
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
470
+ border-radius: 50%;
471
+ display: flex;
472
+ align-items: center;
473
+ justify-content: center;
474
+ }
475
+ .success-icon svg {
476
+ width: 32px;
477
+ height: 32px;
478
+ stroke: white;
479
+ }
480
+ h1 {
481
+ color: #1a1a2e;
482
+ font-size: 24px;
483
+ margin-bottom: 8px;
484
+ }
485
+ .message {
486
+ color: #666;
487
+ font-size: 14px;
488
+ margin-bottom: 24px;
489
+ }
490
+ .spinner {
491
+ width: 24px;
492
+ height: 24px;
493
+ border: 3px solid #e5e7eb;
494
+ border-top-color: #667eea;
495
+ border-radius: 50%;
496
+ animation: spin 1s linear infinite;
497
+ margin: 0 auto 16px;
498
+ }
499
+ @keyframes spin {
500
+ to { transform: rotate(360deg); }
501
+ }
502
+ .redirect-text {
503
+ color: #9ca3af;
504
+ font-size: 13px;
505
+ margin-bottom: 16px;
506
+ }
507
+ .manual-link {
508
+ color: #667eea;
509
+ text-decoration: none;
510
+ font-size: 14px;
511
+ }
512
+ .manual-link:hover {
513
+ text-decoration: underline;
514
+ }
515
+ </style>
516
+ </head>
517
+ <body>
518
+ <div class="container">
519
+ <div class="success-icon">
520
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
521
+ <path d="M20 6L9 17L4 12" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
522
+ </svg>
523
+ </div>
524
+ <h1>Successfully Connected!</h1>
525
+ <p class="message">Your Productive.io credentials have been verified.</p>
526
+ <div class="spinner"></div>
527
+ <p class="redirect-text">Redirecting to Claude Desktop...</p>
528
+ <a href="${escapeHtml(redirectUrl)}" class="manual-link">Click here if not redirected automatically</a>
529
+ </div>
530
+ <script>
531
+ // Redirect after a short delay (backup for meta refresh)
532
+ setTimeout(function() {
533
+ window.location.href = ${JSON.stringify(redirectUrl)};
534
+ }, 2000);
535
+ <\/script>
536
+ </body>
537
+ </html>`;
538
+ }
440
539
  function renderErrorPage(message) {
441
540
  return `<!DOCTYPE html>
442
541
  <html lang="en">
package/dist/oauth.js.map CHANGED
@@ -1 +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
+ {"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 {\n defineEventHandler,\n getQuery,\n readBody,\n sendRedirect,\n setResponseHeader,\n type H3Event,\n} from 'h3';\nimport { createHash } from 'node:crypto';\n\nimport { createAuthToken } from './auth.js';\nimport { createAuthCode, decodeAuthCode } from './crypto.js';\n\n/**\n * OAuth metadata for discovery (RFC 8414)\n * GET /.well-known/oauth-authorization-server\n *\n * MCP clients MUST check this endpoint first for server capabilities.\n */\nexport const oauthMetadataHandler = 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 { orgId, apiToken, userId, redirectUri, state, codeChallenge, codeChallengeMethod } = 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 // Show success page with auto-redirect\n setResponseHeader(event, 'Content-Type', 'text/html; charset=utf-8');\n return renderSuccessPage(redirectUrl.toString());\n});\n\n/**\n * Token endpoint - exchange code for access token\n * POST /token\n *\n * Supports:\n * - authorization_code grant (with PKCE validation)\n * - refresh_token grant\n */\nexport const tokenHandler = 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 success page with auto-redirect\n */\nfunction renderSuccessPage(redirectUrl: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta http-equiv=\"refresh\" content=\"2;url=${escapeHtml(redirectUrl)}\">\n <title>Connected - Productive MCP</title>\n <style>\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n padding: 40px;\n width: 100%;\n max-width: 420px;\n text-align: center;\n }\n .success-icon {\n width: 64px;\n height: 64px;\n margin: 0 auto 24px;\n background: linear-gradient(135deg, #10b981 0%, #059669 100%);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .success-icon svg {\n width: 32px;\n height: 32px;\n stroke: white;\n }\n h1 {\n color: #1a1a2e;\n font-size: 24px;\n margin-bottom: 8px;\n }\n .message {\n color: #666;\n font-size: 14px;\n margin-bottom: 24px;\n }\n .spinner {\n width: 24px;\n height: 24px;\n border: 3px solid #e5e7eb;\n border-top-color: #667eea;\n border-radius: 50%;\n animation: spin 1s linear infinite;\n margin: 0 auto 16px;\n }\n @keyframes spin {\n to { transform: rotate(360deg); }\n }\n .redirect-text {\n color: #9ca3af;\n font-size: 13px;\n margin-bottom: 16px;\n }\n .manual-link {\n color: #667eea;\n text-decoration: none;\n font-size: 14px;\n }\n .manual-link:hover {\n text-decoration: underline;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"success-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M20 6L9 17L4 12\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n <h1>Successfully Connected!</h1>\n <p class=\"message\">Your Productive.io credentials have been verified.</p>\n <div class=\"spinner\"></div>\n <p class=\"redirect-text\">Redirecting to Claude Desktop...</p>\n <a href=\"${escapeHtml(redirectUrl)}\" class=\"manual-link\">Click here if not redirected automatically</a>\n </div>\n <script>\n // Redirect after a short delay (backup for meta refresh)\n setTimeout(function() {\n window.location.href = ${JSON.stringify(redirectUrl)};\n }, 2000);\n </script>\n</body>\n</html>`;\n}\n\n/**\n * Render error page\n */\nfunction renderErrorPage(message: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Error - Productive MCP</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #f3f4f6;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n padding: 40px;\n text-align: center;\n max-width: 400px;\n }\n h1 {\n color: #dc2626;\n margin-bottom: 16px;\n }\n p {\n color: #6b7280;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <h1>Error</h1>\n <p>${escapeHtml(message)}</p>\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&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;AAQtD;;GAEG;AACH,wBAAgB,eAAe,CAC7B,IAAI,GAAE,MAAqB,EAC3B,IAAI,GAAE,MAAqB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAmCjB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,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
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { createServer } from "node:http";
3
2
  import { toNodeListener } from "h3";
3
+ import { createServer } from "node:http";
4
4
  import { createHttpApp } from "./http.js";
5
- import { V as VERSION } from "./version-eQNCcjOb.js";
5
+ import { V as VERSION } from "./version-BBHuTm1A.js";
6
6
  const DEFAULT_PORT = 3e3;
7
7
  const DEFAULT_HOST = "0.0.0.0";
8
8
  function startHttpServer(port = DEFAULT_PORT, host = DEFAULT_HOST) {
@@ -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';\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;"}
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 { toNodeListener } from 'h3';\nimport { createServer, type Server } from 'node:http';\n\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":";;;;;AA6BA,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;"}
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAMzE,MAAM,MAAM,UAAU,GAAG,cAAc,CAAC;AAExC;;GAEG;AACH,wBAAgB,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAEhC;AAED;;GAEG;AACH,wBAAgB,mBAAmB;;;;IAQlC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC;IACjD,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;CAC5E,CAAC,CAiBD;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,UAAU,CAAC,CA6BtB;AAED;;GAEG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,UAAU,CAAC,CAqB/D;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,UAAU,CAAC,CA8BrB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACxD,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;CAC5E,CAAC,CAMD"}
1
+ {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAOzE,MAAM,MAAM,UAAU,GAAG,cAAc,CAAC;AAExC;;GAEG;AACH,wBAAgB,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAEhC;AAED;;GAEG;AACH,wBAAgB,mBAAmB;;;;IAQlC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC;IACjD,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;CAC5E,CAAC,CAiBD;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,UAAU,CAAC,CA6BtB;AAED;;GAEG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,UAAU,CAAC,CAqB/D;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,UAAU,CAAC,CA8BrB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACxD,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;CAC5E,CAAC,CAMD"}
package/dist/stdio.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getConfig, setConfig } from "@studiometa/productive-cli";
2
+ import { e as executeToolWithCredentials } from "./index-CmTDkz-y.js";
2
3
  import { TOOLS, STDIO_ONLY_TOOLS } from "./tools.js";
3
- import { executeToolWithCredentials } from "./handlers.js";
4
4
  function getAvailableTools() {
5
5
  return [...TOOLS, ...STDIO_ONLY_TOOLS];
6
6
  }
package/dist/stdio.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.js","sources":["../src/stdio.ts"],"sourcesContent":["/**\n * Stdio transport handlers for Productive MCP Server\n *\n * This module contains the handler logic for the stdio transport.\n * The actual server startup is in index.ts.\n */\n\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { getConfig, setConfig } from '@studiometa/productive-cli';\n\nimport { TOOLS, STDIO_ONLY_TOOLS } from './tools.js';\nimport { executeToolWithCredentials } from './handlers.js';\n\nexport type ToolResult = CallToolResult;\n\n/**\n * Get all available tools (including stdio-only configuration tools)\n */\nexport function getAvailableTools() {\n return [...TOOLS, ...STDIO_ONLY_TOOLS];\n}\n\n/**\n * Get available prompts\n */\nexport function getAvailablePrompts() {\n return [\n {\n name: 'setup_productive',\n description: 'Interactive setup for Productive.io credentials',\n arguments: [],\n },\n ];\n}\n\n/**\n * Handle the setup_productive prompt\n */\nexport async function handleSetupPrompt(): Promise<{\n messages: Array<{ role: string; content: { type: string; text: string } }>;\n}> {\n const config = await getConfig();\n const hasConfig = !!(config.organizationId && config.apiToken);\n\n return {\n messages: [\n {\n role: 'user',\n content: {\n type: 'text',\n text: hasConfig\n ? 'I have already configured Productive.io credentials. Would you like to update them?'\n : 'I need to configure my Productive.io credentials. Please help me set up:\\n1. Organization ID\\n2. API Token\\n3. User ID (optional)',\n },\n },\n ],\n };\n}\n\n/**\n * Handle the productive_configure tool\n */\nexport async function handleConfigureTool(args: {\n organizationId: string;\n apiToken: string;\n userId?: string;\n}): Promise<ToolResult> {\n const { organizationId, apiToken, userId } = args;\n\n await setConfig('organizationId', organizationId);\n await setConfig('apiToken', apiToken);\n if (userId) {\n await setConfig('userId', userId);\n }\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n {\n success: true,\n message: 'Productive.io credentials configured successfully',\n configured: {\n organizationId,\n userId: userId || 'not set',\n apiToken: '***' + apiToken.slice(-4),\n },\n },\n null,\n 2\n ),\n },\n ],\n };\n}\n\n/**\n * Handle the productive_get_config tool\n */\nexport async function handleGetConfigTool(): Promise<ToolResult> {\n const currentConfig = await getConfig();\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n {\n organizationId: currentConfig.organizationId || 'not configured',\n userId: currentConfig.userId || 'not configured',\n apiToken: currentConfig.apiToken\n ? '***' + currentConfig.apiToken.slice(-4)\n : 'not configured',\n configured: !!(currentConfig.organizationId && currentConfig.apiToken),\n },\n null,\n 2\n ),\n },\n ],\n };\n}\n\n/**\n * Handle a tool call request\n */\nexport async function handleToolCall(\n name: string,\n args: Record<string, unknown>\n): Promise<ToolResult> {\n // Handle stdio-only configuration tools\n if (name === 'productive_configure') {\n return handleConfigureTool(args as Parameters<typeof handleConfigureTool>[0]);\n }\n\n if (name === 'productive_get_config') {\n return handleGetConfigTool();\n }\n\n // Get config for API tools\n const config = await getConfig();\n if (!config.organizationId || !config.apiToken) {\n return {\n content: [\n {\n type: 'text',\n text: 'Error: Productive.io credentials not configured. Please use the \"productive_configure\" tool to set your credentials, or set PRODUCTIVE_ORG_ID and PRODUCTIVE_API_TOKEN environment variables.',\n },\n ],\n isError: true,\n };\n }\n\n // Execute tool with credentials from config\n return executeToolWithCredentials(name, args, {\n organizationId: config.organizationId,\n apiToken: config.apiToken,\n userId: config.userId,\n });\n}\n\n/**\n * Handle a prompt request\n */\nexport async function handlePrompt(name: string): Promise<{\n messages: Array<{ role: string; content: { type: string; text: string } }>;\n}> {\n if (name === 'setup_productive') {\n return handleSetupPrompt();\n }\n\n throw new Error(`Unknown prompt: ${name}`);\n}\n"],"names":[],"mappings":";;;AAkBO,SAAS,oBAAoB;AAClC,SAAO,CAAC,GAAG,OAAO,GAAG,gBAAgB;AACvC;AAKO,SAAS,sBAAsB;AACpC,SAAO;AAAA,IACL;AAAA,MACE,MAAM;AAAA,MACN,aAAa;AAAA,MACb,WAAW,CAAA;AAAA,IAAC;AAAA,EACd;AAEJ;AAKA,eAAsB,oBAEnB;AACD,QAAM,SAAS,MAAM,UAAA;AACrB,QAAM,YAAY,CAAC,EAAE,OAAO,kBAAkB,OAAO;AAErD,SAAO;AAAA,IACL,UAAU;AAAA,MACR;AAAA,QACE,MAAM;AAAA,QACN,SAAS;AAAA,UACP,MAAM;AAAA,UACN,MAAM,YACF,wFACA;AAAA,QAAA;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAEJ;AAKA,eAAsB,oBAAoB,MAIlB;AACtB,QAAM,EAAE,gBAAgB,UAAU,OAAA,IAAW;AAE7C,QAAM,UAAU,kBAAkB,cAAc;AAChD,QAAM,UAAU,YAAY,QAAQ;AACpC,MAAI,QAAQ;AACV,UAAM,UAAU,UAAU,MAAM;AAAA,EAClC;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,MACP;AAAA,QACE,MAAM;AAAA,QACN,MAAM,KAAK;AAAA,UACT;AAAA,YACE,SAAS;AAAA,YACT,SAAS;AAAA,YACT,YAAY;AAAA,cACV;AAAA,cACA,QAAQ,UAAU;AAAA,cAClB,UAAU,QAAQ,SAAS,MAAM,EAAE;AAAA,YAAA;AAAA,UACrC;AAAA,UAEF;AAAA,UACA;AAAA,QAAA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEJ;AAKA,eAAsB,sBAA2C;AAC/D,QAAM,gBAAgB,MAAM,UAAA;AAC5B,SAAO;AAAA,IACL,SAAS;AAAA,MACP;AAAA,QACE,MAAM;AAAA,QACN,MAAM,KAAK;AAAA,UACT;AAAA,YACE,gBAAgB,cAAc,kBAAkB;AAAA,YAChD,QAAQ,cAAc,UAAU;AAAA,YAChC,UAAU,cAAc,WACpB,QAAQ,cAAc,SAAS,MAAM,EAAE,IACvC;AAAA,YACJ,YAAY,CAAC,EAAE,cAAc,kBAAkB,cAAc;AAAA,UAAA;AAAA,UAE/D;AAAA,UACA;AAAA,QAAA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEJ;AAKA,eAAsB,eACpB,MACA,MACqB;AAErB,MAAI,SAAS,wBAAwB;AACnC,WAAO,oBAAoB,IAAiD;AAAA,EAC9E;AAEA,MAAI,SAAS,yBAAyB;AACpC,WAAO,oBAAA;AAAA,EACT;AAGA,QAAM,SAAS,MAAM,UAAA;AACrB,MAAI,CAAC,OAAO,kBAAkB,CAAC,OAAO,UAAU;AAC9C,WAAO;AAAA,MACL,SAAS;AAAA,QACP;AAAA,UACE,MAAM;AAAA,UACN,MAAM;AAAA,QAAA;AAAA,MACR;AAAA,MAEF,SAAS;AAAA,IAAA;AAAA,EAEb;AAGA,SAAO,2BAA2B,MAAM,MAAM;AAAA,IAC5C,gBAAgB,OAAO;AAAA,IACvB,UAAU,OAAO;AAAA,IACjB,QAAQ,OAAO;AAAA,EAAA,CAChB;AACH;AAKA,eAAsB,aAAa,MAEhC;AACD,MAAI,SAAS,oBAAoB;AAC/B,WAAO,kBAAA;AAAA,EACT;AAEA,QAAM,IAAI,MAAM,mBAAmB,IAAI,EAAE;AAC3C;"}
1
+ {"version":3,"file":"stdio.js","sources":["../src/stdio.ts"],"sourcesContent":["/**\n * Stdio transport handlers for Productive MCP Server\n *\n * This module contains the handler logic for the stdio transport.\n * The actual server startup is in index.ts.\n */\n\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\nimport { getConfig, setConfig } from '@studiometa/productive-cli';\n\nimport { executeToolWithCredentials } from './handlers.js';\nimport { TOOLS, STDIO_ONLY_TOOLS } from './tools.js';\n\nexport type ToolResult = CallToolResult;\n\n/**\n * Get all available tools (including stdio-only configuration tools)\n */\nexport function getAvailableTools() {\n return [...TOOLS, ...STDIO_ONLY_TOOLS];\n}\n\n/**\n * Get available prompts\n */\nexport function getAvailablePrompts() {\n return [\n {\n name: 'setup_productive',\n description: 'Interactive setup for Productive.io credentials',\n arguments: [],\n },\n ];\n}\n\n/**\n * Handle the setup_productive prompt\n */\nexport async function handleSetupPrompt(): Promise<{\n messages: Array<{ role: string; content: { type: string; text: string } }>;\n}> {\n const config = await getConfig();\n const hasConfig = !!(config.organizationId && config.apiToken);\n\n return {\n messages: [\n {\n role: 'user',\n content: {\n type: 'text',\n text: hasConfig\n ? 'I have already configured Productive.io credentials. Would you like to update them?'\n : 'I need to configure my Productive.io credentials. Please help me set up:\\n1. Organization ID\\n2. API Token\\n3. User ID (optional)',\n },\n },\n ],\n };\n}\n\n/**\n * Handle the productive_configure tool\n */\nexport async function handleConfigureTool(args: {\n organizationId: string;\n apiToken: string;\n userId?: string;\n}): Promise<ToolResult> {\n const { organizationId, apiToken, userId } = args;\n\n await setConfig('organizationId', organizationId);\n await setConfig('apiToken', apiToken);\n if (userId) {\n await setConfig('userId', userId);\n }\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n {\n success: true,\n message: 'Productive.io credentials configured successfully',\n configured: {\n organizationId,\n userId: userId || 'not set',\n apiToken: '***' + apiToken.slice(-4),\n },\n },\n null,\n 2,\n ),\n },\n ],\n };\n}\n\n/**\n * Handle the productive_get_config tool\n */\nexport async function handleGetConfigTool(): Promise<ToolResult> {\n const currentConfig = await getConfig();\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n {\n organizationId: currentConfig.organizationId || 'not configured',\n userId: currentConfig.userId || 'not configured',\n apiToken: currentConfig.apiToken\n ? '***' + currentConfig.apiToken.slice(-4)\n : 'not configured',\n configured: !!(currentConfig.organizationId && currentConfig.apiToken),\n },\n null,\n 2,\n ),\n },\n ],\n };\n}\n\n/**\n * Handle a tool call request\n */\nexport async function handleToolCall(\n name: string,\n args: Record<string, unknown>,\n): Promise<ToolResult> {\n // Handle stdio-only configuration tools\n if (name === 'productive_configure') {\n return handleConfigureTool(args as Parameters<typeof handleConfigureTool>[0]);\n }\n\n if (name === 'productive_get_config') {\n return handleGetConfigTool();\n }\n\n // Get config for API tools\n const config = await getConfig();\n if (!config.organizationId || !config.apiToken) {\n return {\n content: [\n {\n type: 'text',\n text: 'Error: Productive.io credentials not configured. Please use the \"productive_configure\" tool to set your credentials, or set PRODUCTIVE_ORG_ID and PRODUCTIVE_API_TOKEN environment variables.',\n },\n ],\n isError: true,\n };\n }\n\n // Execute tool with credentials from config\n return executeToolWithCredentials(name, args, {\n organizationId: config.organizationId,\n apiToken: config.apiToken,\n userId: config.userId,\n });\n}\n\n/**\n * Handle a prompt request\n */\nexport async function handlePrompt(name: string): Promise<{\n messages: Array<{ role: string; content: { type: string; text: string } }>;\n}> {\n if (name === 'setup_productive') {\n return handleSetupPrompt();\n }\n\n throw new Error(`Unknown prompt: ${name}`);\n}\n"],"names":[],"mappings":";;;AAmBO,SAAS,oBAAoB;AAClC,SAAO,CAAC,GAAG,OAAO,GAAG,gBAAgB;AACvC;AAKO,SAAS,sBAAsB;AACpC,SAAO;AAAA,IACL;AAAA,MACE,MAAM;AAAA,MACN,aAAa;AAAA,MACb,WAAW,CAAA;AAAA,IAAC;AAAA,EACd;AAEJ;AAKA,eAAsB,oBAEnB;AACD,QAAM,SAAS,MAAM,UAAA;AACrB,QAAM,YAAY,CAAC,EAAE,OAAO,kBAAkB,OAAO;AAErD,SAAO;AAAA,IACL,UAAU;AAAA,MACR;AAAA,QACE,MAAM;AAAA,QACN,SAAS;AAAA,UACP,MAAM;AAAA,UACN,MAAM,YACF,wFACA;AAAA,QAAA;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAEJ;AAKA,eAAsB,oBAAoB,MAIlB;AACtB,QAAM,EAAE,gBAAgB,UAAU,OAAA,IAAW;AAE7C,QAAM,UAAU,kBAAkB,cAAc;AAChD,QAAM,UAAU,YAAY,QAAQ;AACpC,MAAI,QAAQ;AACV,UAAM,UAAU,UAAU,MAAM;AAAA,EAClC;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,MACP;AAAA,QACE,MAAM;AAAA,QACN,MAAM,KAAK;AAAA,UACT;AAAA,YACE,SAAS;AAAA,YACT,SAAS;AAAA,YACT,YAAY;AAAA,cACV;AAAA,cACA,QAAQ,UAAU;AAAA,cAClB,UAAU,QAAQ,SAAS,MAAM,EAAE;AAAA,YAAA;AAAA,UACrC;AAAA,UAEF;AAAA,UACA;AAAA,QAAA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEJ;AAKA,eAAsB,sBAA2C;AAC/D,QAAM,gBAAgB,MAAM,UAAA;AAC5B,SAAO;AAAA,IACL,SAAS;AAAA,MACP;AAAA,QACE,MAAM;AAAA,QACN,MAAM,KAAK;AAAA,UACT;AAAA,YACE,gBAAgB,cAAc,kBAAkB;AAAA,YAChD,QAAQ,cAAc,UAAU;AAAA,YAChC,UAAU,cAAc,WACpB,QAAQ,cAAc,SAAS,MAAM,EAAE,IACvC;AAAA,YACJ,YAAY,CAAC,EAAE,cAAc,kBAAkB,cAAc;AAAA,UAAA;AAAA,UAE/D;AAAA,UACA;AAAA,QAAA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEJ;AAKA,eAAsB,eACpB,MACA,MACqB;AAErB,MAAI,SAAS,wBAAwB;AACnC,WAAO,oBAAoB,IAAiD;AAAA,EAC9E;AAEA,MAAI,SAAS,yBAAyB;AACpC,WAAO,oBAAA;AAAA,EACT;AAGA,QAAM,SAAS,MAAM,UAAA;AACrB,MAAI,CAAC,OAAO,kBAAkB,CAAC,OAAO,UAAU;AAC9C,WAAO;AAAA,MACL,SAAS;AAAA,QACP;AAAA,UACE,MAAM;AAAA,UACN,MAAM;AAAA,QAAA;AAAA,MACR;AAAA,MAEF,SAAS;AAAA,IAAA;AAAA,EAEb;AAGA,SAAO,2BAA2B,MAAM,MAAM;AAAA,IAC5C,gBAAgB,OAAO;AAAA,IACvB,UAAU,OAAO;AAAA,IACjB,QAAQ,OAAO;AAAA,EAAA,CAChB;AACH;AAKA,eAAsB,aAAa,MAEhC;AACD,MAAI,SAAS,oBAAoB;AAC/B,WAAO,kBAAA;AAAA,EACT;AAEA,QAAM,IAAI,MAAM,mBAAmB,IAAI,EAAE;AAC3C;"}
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAE/D;;;;GAIG;AACH,eAAO,MAAM,KAAK,EAAE,IAAI,EAgCvB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,IAAI,EAsBlC,CAAC"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAE/D;;;;GAIG;AACH,eAAO,MAAM,KAAK,EAAE,IAAI,EA6DvB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,IAAI,EAsBlC,CAAC"}
package/dist/tools.js CHANGED
@@ -1,30 +1,59 @@
1
1
  const TOOLS = [
2
2
  {
3
3
  name: "productive",
4
- description: "Productive.io API. Resources: projects, time, tasks, services, people. Actions: list, get, create/update/delete (time), me (people). Filters: project_id, person_id, service_id, after/before (dates).",
4
+ description: "Productive.io API. Resources: projects, time, tasks, services, people, companies, comments, timers, deals, bookings. Actions: list, get, create, update (varies by resource), start/stop (timers), me (people). Filters: project_id, person_id, service_id, company_id, after/before (dates).",
5
5
  inputSchema: {
6
6
  type: "object",
7
7
  properties: {
8
8
  resource: {
9
9
  type: "string",
10
- enum: ["projects", "time", "tasks", "services", "people"]
10
+ enum: [
11
+ "projects",
12
+ "time",
13
+ "tasks",
14
+ "services",
15
+ "people",
16
+ "companies",
17
+ "comments",
18
+ "timers",
19
+ "deals",
20
+ "bookings"
21
+ ]
11
22
  },
12
23
  action: {
13
24
  type: "string",
14
- enum: ["list", "get", "create", "update", "delete", "me"]
25
+ enum: ["list", "get", "create", "update", "me", "start", "stop"]
15
26
  },
16
27
  id: { type: "string" },
17
28
  filter: { type: "object" },
18
29
  page: { type: "number" },
19
30
  per_page: { type: "number" },
20
31
  compact: { type: "boolean" },
21
- // Time entry fields
32
+ // Common fields
22
33
  person_id: { type: "string" },
23
34
  service_id: { type: "string" },
24
35
  task_id: { type: "string" },
36
+ company_id: { type: "string" },
25
37
  time: { type: "number" },
26
38
  date: { type: "string" },
27
- note: { type: "string" }
39
+ note: { type: "string" },
40
+ // Task fields
41
+ title: { type: "string" },
42
+ project_id: { type: "string" },
43
+ task_list_id: { type: "string" },
44
+ description: { type: "string" },
45
+ assignee_id: { type: "string" },
46
+ // Company fields
47
+ name: { type: "string" },
48
+ // Comment fields
49
+ body: { type: "string" },
50
+ deal_id: { type: "string" },
51
+ // Timer fields
52
+ time_entry_id: { type: "string" },
53
+ // Booking fields
54
+ started_on: { type: "string" },
55
+ ended_on: { type: "string" },
56
+ event_id: { type: "string" }
28
57
  },
29
58
  required: ["resource", "action"]
30
59
  }
package/dist/tools.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tools.js","sources":["../src/tools.ts"],"sourcesContent":["import type { Tool } from '@modelcontextprotocol/sdk/types.js';\n\n/**\n * Single consolidated tool for Productive.io MCP server\n *\n * Optimized for minimal token overhead (~170 tokens vs ~1300 for individual tools)\n */\nexport const TOOLS: Tool[] = [\n {\n name: 'productive',\n description:\n 'Productive.io API. Resources: projects, time, tasks, services, people. Actions: list, get, create/update/delete (time), me (people). Filters: project_id, person_id, service_id, after/before (dates).',\n inputSchema: {\n type: 'object',\n properties: {\n resource: {\n type: 'string',\n enum: ['projects', 'time', 'tasks', 'services', 'people'],\n },\n action: {\n type: 'string',\n enum: ['list', 'get', 'create', 'update', 'delete', 'me'],\n },\n id: { type: 'string' },\n filter: { type: 'object' },\n page: { type: 'number' },\n per_page: { type: 'number' },\n compact: { type: 'boolean' },\n // Time entry fields\n person_id: { type: 'string' },\n service_id: { type: 'string' },\n task_id: { type: 'string' },\n time: { type: 'number' },\n date: { type: 'string' },\n note: { type: 'string' },\n },\n required: ['resource', 'action'],\n },\n },\n];\n\n/**\n * Additional tools only available in stdio mode (local execution)\n * These tools manage persistent configuration\n */\nexport const STDIO_ONLY_TOOLS: Tool[] = [\n {\n name: 'productive_configure',\n description: 'Configure Productive.io credentials',\n inputSchema: {\n type: 'object',\n properties: {\n organizationId: { type: 'string' },\n apiToken: { type: 'string' },\n userId: { type: 'string' },\n },\n required: ['organizationId', 'apiToken'],\n },\n },\n {\n name: 'productive_get_config',\n description: 'Get current configuration',\n inputSchema: {\n type: 'object',\n properties: {},\n },\n },\n];\n"],"names":[],"mappings":"AAOO,MAAM,QAAgB;AAAA,EAC3B;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IACF,aAAa;AAAA,MACX,MAAM;AAAA,MACN,YAAY;AAAA,QACV,UAAU;AAAA,UACR,MAAM;AAAA,UACN,MAAM,CAAC,YAAY,QAAQ,SAAS,YAAY,QAAQ;AAAA,QAAA;AAAA,QAE1D,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,MAAM,CAAC,QAAQ,OAAO,UAAU,UAAU,UAAU,IAAI;AAAA,QAAA;AAAA,QAE1D,IAAI,EAAE,MAAM,SAAA;AAAA,QACZ,QAAQ,EAAE,MAAM,SAAA;AAAA,QAChB,MAAM,EAAE,MAAM,SAAA;AAAA,QACd,UAAU,EAAE,MAAM,SAAA;AAAA,QAClB,SAAS,EAAE,MAAM,UAAA;AAAA;AAAA,QAEjB,WAAW,EAAE,MAAM,SAAA;AAAA,QACnB,YAAY,EAAE,MAAM,SAAA;AAAA,QACpB,SAAS,EAAE,MAAM,SAAA;AAAA,QACjB,MAAM,EAAE,MAAM,SAAA;AAAA,QACd,MAAM,EAAE,MAAM,SAAA;AAAA,QACd,MAAM,EAAE,MAAM,SAAA;AAAA,MAAS;AAAA,MAEzB,UAAU,CAAC,YAAY,QAAQ;AAAA,IAAA;AAAA,EACjC;AAEJ;AAMO,MAAM,mBAA2B;AAAA,EACtC;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,MACX,MAAM;AAAA,MACN,YAAY;AAAA,QACV,gBAAgB,EAAE,MAAM,SAAA;AAAA,QACxB,UAAU,EAAE,MAAM,SAAA;AAAA,QAClB,QAAQ,EAAE,MAAM,SAAA;AAAA,MAAS;AAAA,MAE3B,UAAU,CAAC,kBAAkB,UAAU;AAAA,IAAA;AAAA,EACzC;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,MACX,MAAM;AAAA,MACN,YAAY,CAAA;AAAA,IAAC;AAAA,EACf;AAEJ;"}
1
+ {"version":3,"file":"tools.js","sources":["../src/tools.ts"],"sourcesContent":["import type { Tool } from '@modelcontextprotocol/sdk/types.js';\n\n/**\n * Single consolidated tool for Productive.io MCP server\n *\n * Optimized for minimal token overhead (~170 tokens vs ~1300 for individual tools)\n */\nexport const TOOLS: Tool[] = [\n {\n name: 'productive',\n description:\n 'Productive.io API. Resources: projects, time, tasks, services, people, companies, comments, timers, deals, bookings. Actions: list, get, create, update (varies by resource), start/stop (timers), me (people). Filters: project_id, person_id, service_id, company_id, after/before (dates).',\n inputSchema: {\n type: 'object',\n properties: {\n resource: {\n type: 'string',\n enum: [\n 'projects',\n 'time',\n 'tasks',\n 'services',\n 'people',\n 'companies',\n 'comments',\n 'timers',\n 'deals',\n 'bookings',\n ],\n },\n action: {\n type: 'string',\n enum: ['list', 'get', 'create', 'update', 'me', 'start', 'stop'],\n },\n id: { type: 'string' },\n filter: { type: 'object' },\n page: { type: 'number' },\n per_page: { type: 'number' },\n compact: { type: 'boolean' },\n // Common fields\n person_id: { type: 'string' },\n service_id: { type: 'string' },\n task_id: { type: 'string' },\n company_id: { type: 'string' },\n time: { type: 'number' },\n date: { type: 'string' },\n note: { type: 'string' },\n // Task fields\n title: { type: 'string' },\n project_id: { type: 'string' },\n task_list_id: { type: 'string' },\n description: { type: 'string' },\n assignee_id: { type: 'string' },\n // Company fields\n name: { type: 'string' },\n // Comment fields\n body: { type: 'string' },\n deal_id: { type: 'string' },\n // Timer fields\n time_entry_id: { type: 'string' },\n // Booking fields\n started_on: { type: 'string' },\n ended_on: { type: 'string' },\n event_id: { type: 'string' },\n },\n required: ['resource', 'action'],\n },\n },\n];\n\n/**\n * Additional tools only available in stdio mode (local execution)\n * These tools manage persistent configuration\n */\nexport const STDIO_ONLY_TOOLS: Tool[] = [\n {\n name: 'productive_configure',\n description: 'Configure Productive.io credentials',\n inputSchema: {\n type: 'object',\n properties: {\n organizationId: { type: 'string' },\n apiToken: { type: 'string' },\n userId: { type: 'string' },\n },\n required: ['organizationId', 'apiToken'],\n },\n },\n {\n name: 'productive_get_config',\n description: 'Get current configuration',\n inputSchema: {\n type: 'object',\n properties: {},\n },\n },\n];\n"],"names":[],"mappings":"AAOO,MAAM,QAAgB;AAAA,EAC3B;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IACF,aAAa;AAAA,MACX,MAAM;AAAA,MACN,YAAY;AAAA,QACV,UAAU;AAAA,UACR,MAAM;AAAA,UACN,MAAM;AAAA,YACJ;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UAAA;AAAA,QACF;AAAA,QAEF,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,MAAM,CAAC,QAAQ,OAAO,UAAU,UAAU,MAAM,SAAS,MAAM;AAAA,QAAA;AAAA,QAEjE,IAAI,EAAE,MAAM,SAAA;AAAA,QACZ,QAAQ,EAAE,MAAM,SAAA;AAAA,QAChB,MAAM,EAAE,MAAM,SAAA;AAAA,QACd,UAAU,EAAE,MAAM,SAAA;AAAA,QAClB,SAAS,EAAE,MAAM,UAAA;AAAA;AAAA,QAEjB,WAAW,EAAE,MAAM,SAAA;AAAA,QACnB,YAAY,EAAE,MAAM,SAAA;AAAA,QACpB,SAAS,EAAE,MAAM,SAAA;AAAA,QACjB,YAAY,EAAE,MAAM,SAAA;AAAA,QACpB,MAAM,EAAE,MAAM,SAAA;AAAA,QACd,MAAM,EAAE,MAAM,SAAA;AAAA,QACd,MAAM,EAAE,MAAM,SAAA;AAAA;AAAA,QAEd,OAAO,EAAE,MAAM,SAAA;AAAA,QACf,YAAY,EAAE,MAAM,SAAA;AAAA,QACpB,cAAc,EAAE,MAAM,SAAA;AAAA,QACtB,aAAa,EAAE,MAAM,SAAA;AAAA,QACrB,aAAa,EAAE,MAAM,SAAA;AAAA;AAAA,QAErB,MAAM,EAAE,MAAM,SAAA;AAAA;AAAA,QAEd,MAAM,EAAE,MAAM,SAAA;AAAA,QACd,SAAS,EAAE,MAAM,SAAA;AAAA;AAAA,QAEjB,eAAe,EAAE,MAAM,SAAA;AAAA;AAAA,QAEvB,YAAY,EAAE,MAAM,SAAA;AAAA,QACpB,UAAU,EAAE,MAAM,SAAA;AAAA,QAClB,UAAU,EAAE,MAAM,SAAA;AAAA,MAAS;AAAA,MAE7B,UAAU,CAAC,YAAY,QAAQ;AAAA,IAAA;AAAA,EACjC;AAEJ;AAMO,MAAM,mBAA2B;AAAA,EACtC;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,MACX,MAAM;AAAA,MACN,YAAY;AAAA,QACV,gBAAgB,EAAE,MAAM,SAAA;AAAA,QACxB,UAAU,EAAE,MAAM,SAAA;AAAA,QAClB,QAAQ,EAAE,MAAM,SAAA;AAAA,MAAS;AAAA,MAE3B,UAAU,CAAC,kBAAkB,UAAU;AAAA,IAAA;AAAA,EACzC;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,MACX,MAAM;AAAA,MACN,YAAY,CAAA;AAAA,IAAC;AAAA,EACf;AAEJ;"}
@@ -0,0 +1,5 @@
1
+ const VERSION = "0.6.1";
2
+ export {
3
+ VERSION as V
4
+ };
5
+ //# sourceMappingURL=version-BBHuTm1A.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version-eQNCcjOb.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;"}
1
+ {"version":3,"file":"version-BBHuTm1A.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;"}