@studiometa/productive-mcp 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +29 -0
- package/LICENSE +21 -0
- package/README.md +352 -0
- package/dist/auth.d.ts +25 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +40 -0
- package/dist/auth.js.map +1 -0
- package/dist/formatters.d.ts +42 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/handlers.d.ts +17 -0
- package/dist/handlers.d.ts.map +1 -0
- package/dist/handlers.js +137 -0
- package/dist/handlers.js.map +1 -0
- package/dist/http.d.ts +89 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +131 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +27 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +41 -0
- package/dist/server.js.map +1 -0
- package/dist/stdio.d.ts +100 -0
- package/dist/stdio.d.ts.map +1 -0
- package/dist/stdio.js +119 -0
- package/dist/stdio.js.map +1 -0
- package/dist/tools.d.ts +12 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +227 -0
- package/dist/tools.js.map +1 -0
- package/package.json +87 -0
package/Dockerfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Productive MCP Server - HTTP Transport
|
|
2
|
+
#
|
|
3
|
+
# Build from the repository root:
|
|
4
|
+
# docker build -t productive-mcp-server -f packages/productive-mcp/Dockerfile .
|
|
5
|
+
#
|
|
6
|
+
# Or from the package directory with published packages:
|
|
7
|
+
# docker build -t productive-mcp-server .
|
|
8
|
+
|
|
9
|
+
FROM node:20-alpine
|
|
10
|
+
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
|
|
13
|
+
# Install the package globally from npm
|
|
14
|
+
RUN npm install -g @studiometa/productive-mcp
|
|
15
|
+
|
|
16
|
+
# Set environment variables
|
|
17
|
+
ENV NODE_ENV=production
|
|
18
|
+
ENV PORT=3000
|
|
19
|
+
ENV HOST=0.0.0.0
|
|
20
|
+
|
|
21
|
+
# Expose port
|
|
22
|
+
EXPOSE 3000
|
|
23
|
+
|
|
24
|
+
# Health check
|
|
25
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
26
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
|
27
|
+
|
|
28
|
+
# Run the HTTP server
|
|
29
|
+
CMD ["productive-mcp-server"]
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Studio Meta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# @studiometa/productive-mcp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@studiometa/productive-mcp)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
MCP (Model Context Protocol) server for [Productive.io](https://productive.io) API integration. Enables Claude Desktop and other MCP clients to interact with Productive.io for project management, time tracking, and task management.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- ✅ Full Productive.io API access via MCP
|
|
11
|
+
- 🔧 Support for projects, tasks, time entries, services, and people
|
|
12
|
+
- 🔐 Two modes: local (stdio) and remote (HTTP)
|
|
13
|
+
- 🌐 Deploy once, share with your team via Claude Desktop custom connectors
|
|
14
|
+
- 🐳 Docker-ready for easy deployment
|
|
15
|
+
- 📦 Built on [@studiometa/productive-cli](../productive-cli)
|
|
16
|
+
|
|
17
|
+
## Usage Modes
|
|
18
|
+
|
|
19
|
+
This package supports two modes:
|
|
20
|
+
|
|
21
|
+
| Mode | Command | Use Case |
|
|
22
|
+
|------|---------|----------|
|
|
23
|
+
| **Local (stdio)** | `productive-mcp` | Personal use via Claude Desktop config |
|
|
24
|
+
| **Remote (HTTP)** | `productive-mcp-server` | Team use via Claude Desktop custom connector |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Mode 1: Local (stdio) - Personal Use
|
|
29
|
+
|
|
30
|
+
Use this mode when you want to run the MCP server locally on your machine.
|
|
31
|
+
|
|
32
|
+
### Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g @studiometa/productive-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Claude Desktop Configuration
|
|
39
|
+
|
|
40
|
+
Edit your Claude Desktop config:
|
|
41
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
42
|
+
- **Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
|
|
43
|
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"productive": {
|
|
49
|
+
"command": "productive-mcp"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Restart Claude Desktop, then ask Claude:
|
|
56
|
+
|
|
57
|
+
> "Configure my Productive.io credentials"
|
|
58
|
+
|
|
59
|
+
Claude will guide you through the setup.
|
|
60
|
+
|
|
61
|
+
### Alternative: Environment Variables
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"productive": {
|
|
67
|
+
"command": "productive-mcp",
|
|
68
|
+
"env": {
|
|
69
|
+
"PRODUCTIVE_ORG_ID": "your-org-id",
|
|
70
|
+
"PRODUCTIVE_API_TOKEN": "your-auth-token",
|
|
71
|
+
"PRODUCTIVE_USER_ID": "your-user-id"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Mode 2: Remote (HTTP) - Team Use
|
|
81
|
+
|
|
82
|
+
Deploy once, share with your entire team via Claude Desktop's **custom connector** feature.
|
|
83
|
+
|
|
84
|
+
### How It Works
|
|
85
|
+
|
|
86
|
+
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
|
|
89
|
+
|
|
90
|
+
### Deploy the Server
|
|
91
|
+
|
|
92
|
+
#### Option A: Docker
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Build
|
|
96
|
+
docker build -t productive-mcp-server -f packages/productive-mcp/Dockerfile .
|
|
97
|
+
|
|
98
|
+
# Run
|
|
99
|
+
docker run -d \
|
|
100
|
+
--name productive-mcp-server \
|
|
101
|
+
--restart unless-stopped \
|
|
102
|
+
-p 3000:3000 \
|
|
103
|
+
productive-mcp-server
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Option B: Node.js
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm install -g @studiometa/productive-mcp
|
|
110
|
+
|
|
111
|
+
# Start server
|
|
112
|
+
PORT=3000 productive-mcp-server
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Option C: Docker Compose
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
version: '3.8'
|
|
119
|
+
|
|
120
|
+
services:
|
|
121
|
+
productive-mcp:
|
|
122
|
+
build:
|
|
123
|
+
context: .
|
|
124
|
+
dockerfile: packages/productive-mcp/Dockerfile
|
|
125
|
+
restart: unless-stopped
|
|
126
|
+
ports:
|
|
127
|
+
- "3000:3000"
|
|
128
|
+
environment:
|
|
129
|
+
PORT: 3000
|
|
130
|
+
HOST: 0.0.0.0
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Generate Your Token
|
|
134
|
+
|
|
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
|
+
```
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
```bash
|
|
144
|
+
echo -n "12345:pk_abc123xyz:67890" | base64
|
|
145
|
+
# Output: MTIzNDU6cGtfYWJjMTIzeHl6OjY3ODkw
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Configure Claude Desktop Custom Connector
|
|
149
|
+
|
|
150
|
+
1. Open Claude Desktop Settings
|
|
151
|
+
2. Go to **Custom Connectors** (beta)
|
|
152
|
+
3. Click **Add custom connector**
|
|
153
|
+
4. Configure:
|
|
154
|
+
- **Name**: `Productive`
|
|
155
|
+
- **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
|
|
158
|
+
|
|
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.
|
|
160
|
+
|
|
161
|
+
### Server Endpoints
|
|
162
|
+
|
|
163
|
+
| Endpoint | Method | Description |
|
|
164
|
+
|----------|--------|-------------|
|
|
165
|
+
| `/mcp` | POST | MCP JSON-RPC endpoint |
|
|
166
|
+
| `/health` | GET | Health check |
|
|
167
|
+
| `/` | GET | Server info |
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Available Tools
|
|
172
|
+
|
|
173
|
+
### Projects
|
|
174
|
+
- `productive_list_projects` - List projects with optional filters
|
|
175
|
+
- `productive_get_project` - Get project details by ID
|
|
176
|
+
|
|
177
|
+
### Tasks
|
|
178
|
+
- `productive_list_tasks` - List tasks with optional filters
|
|
179
|
+
- `productive_get_task` - Get task details by ID
|
|
180
|
+
|
|
181
|
+
### Time Entries
|
|
182
|
+
- `productive_list_time_entries` - List time entries with filters
|
|
183
|
+
- `productive_get_time_entry` - Get time entry details by ID
|
|
184
|
+
- `productive_create_time_entry` - Create a new time entry
|
|
185
|
+
- `productive_update_time_entry` - Update an existing time entry
|
|
186
|
+
- `productive_delete_time_entry` - Delete a time entry
|
|
187
|
+
|
|
188
|
+
### Services
|
|
189
|
+
- `productive_list_services` - List services (budget line items)
|
|
190
|
+
|
|
191
|
+
### People
|
|
192
|
+
- `productive_list_people` - List people in the organization
|
|
193
|
+
- `productive_get_person` - Get person details by ID
|
|
194
|
+
- `productive_get_current_user` - Get current authenticated user
|
|
195
|
+
|
|
196
|
+
### Configuration (Local mode only)
|
|
197
|
+
- `productive_configure` - Configure credentials
|
|
198
|
+
- `productive_get_config` - View current configuration
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Get Your Productive.io Credentials
|
|
203
|
+
|
|
204
|
+
1. Log into [Productive.io](https://productive.io)
|
|
205
|
+
2. Go to **Settings → Integrations → API**
|
|
206
|
+
3. Generate an API token
|
|
207
|
+
4. Note your Organization ID (visible in URL or API settings)
|
|
208
|
+
5. Note your User ID (click your profile, visible in URL)
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Usage Examples
|
|
213
|
+
|
|
214
|
+
### First Time Setup (Local Mode)
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
You: "Configure my Productive.io credentials"
|
|
218
|
+
Claude: "I'll help you set up. Please provide your Organization ID and API Token..."
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Common Queries
|
|
222
|
+
|
|
223
|
+
- "Show me all active projects in Productive"
|
|
224
|
+
- "Create a time entry for 2 hours today on project X"
|
|
225
|
+
- "List all tasks assigned to me"
|
|
226
|
+
- "What did I work on last week?"
|
|
227
|
+
- "Show me the services/budgets for project 12345"
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Development
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# Clone the repository
|
|
235
|
+
git clone https://github.com/studiometa/productive-cli
|
|
236
|
+
cd productive-cli
|
|
237
|
+
|
|
238
|
+
# Install dependencies
|
|
239
|
+
npm install
|
|
240
|
+
|
|
241
|
+
# Build all packages
|
|
242
|
+
npm run build
|
|
243
|
+
|
|
244
|
+
# Or build only MCP package
|
|
245
|
+
npm run build -w @studiometa/productive-mcp
|
|
246
|
+
|
|
247
|
+
# Development mode (watch)
|
|
248
|
+
npm run dev -w @studiometa/productive-mcp
|
|
249
|
+
|
|
250
|
+
# Test local server
|
|
251
|
+
node packages/productive-mcp/dist/index.js
|
|
252
|
+
|
|
253
|
+
# Test HTTP server
|
|
254
|
+
node packages/productive-mcp/dist/server.js
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Testing the HTTP Server
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
# Start the server
|
|
261
|
+
PORT=3000 node packages/productive-mcp/dist/server.js
|
|
262
|
+
|
|
263
|
+
# Generate a test token
|
|
264
|
+
TOKEN=$(echo -n "YOUR_ORG_ID:YOUR_API_TOKEN:YOUR_USER_ID" | base64)
|
|
265
|
+
|
|
266
|
+
# Test health endpoint
|
|
267
|
+
curl http://localhost:3000/health
|
|
268
|
+
|
|
269
|
+
# Test MCP endpoint
|
|
270
|
+
curl -X POST http://localhost:3000/mcp \
|
|
271
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
272
|
+
-H "Content-Type: application/json" \
|
|
273
|
+
-d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":1}'
|
|
274
|
+
|
|
275
|
+
# List projects
|
|
276
|
+
curl -X POST http://localhost:3000/mcp \
|
|
277
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
278
|
+
-H "Content-Type: application/json" \
|
|
279
|
+
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"productive_list_projects","arguments":{}},"id":2}'
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Troubleshooting
|
|
285
|
+
|
|
286
|
+
### Local mode: Credentials not found
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# Check environment variables
|
|
290
|
+
echo $PRODUCTIVE_ORG_ID
|
|
291
|
+
echo $PRODUCTIVE_API_TOKEN
|
|
292
|
+
|
|
293
|
+
# Or use the configure tool via Claude
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### HTTP mode: 401 Unauthorized
|
|
297
|
+
|
|
298
|
+
- Verify your token is correctly base64-encoded
|
|
299
|
+
- Check that orgId:apiToken:userId are separated by colons
|
|
300
|
+
- Ensure no newlines in the base64 output
|
|
301
|
+
|
|
302
|
+
### Docker: View logs
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
docker logs productive-mcp-server -f
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Test server manually (local mode)
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
echo '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":1}' | productive-mcp
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Requirements
|
|
317
|
+
|
|
318
|
+
- Node.js 20+
|
|
319
|
+
- Productive.io account with API access
|
|
320
|
+
- Docker (optional, for server deployment)
|
|
321
|
+
|
|
322
|
+
## Architecture
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
productive-mcp/
|
|
326
|
+
├── src/
|
|
327
|
+
│ ├── index.ts # Stdio transport (local mode)
|
|
328
|
+
│ ├── server.ts # HTTP transport (remote mode)
|
|
329
|
+
│ ├── tools.ts # Tool definitions (shared)
|
|
330
|
+
│ ├── handlers.ts # Tool execution (shared)
|
|
331
|
+
│ └── auth.ts # Bearer token parsing
|
|
332
|
+
├── Dockerfile
|
|
333
|
+
└── README.md
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Related Packages
|
|
337
|
+
|
|
338
|
+
- [@studiometa/productive-cli](../productive-cli) - CLI tool for Productive.io
|
|
339
|
+
- [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/sdk) - Official MCP SDK
|
|
340
|
+
- [h3](https://github.com/unjs/h3) - HTTP framework for the server
|
|
341
|
+
|
|
342
|
+
## License
|
|
343
|
+
|
|
344
|
+
MIT © [Studio Meta](https://www.studiometa.fr)
|
|
345
|
+
|
|
346
|
+
## Links
|
|
347
|
+
|
|
348
|
+
- [GitHub Repository](https://github.com/studiometa/productive-cli)
|
|
349
|
+
- [Productive.io API Docs](https://developer.productive.io)
|
|
350
|
+
- [MCP Documentation](https://modelcontextprotocol.io)
|
|
351
|
+
- [Claude Desktop Custom Connectors](https://docs.anthropic.com)
|
|
352
|
+
- [Issues](https://github.com/studiometa/productive-cli/issues)
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication utilities for Productive MCP server
|
|
3
|
+
*/
|
|
4
|
+
export interface ProductiveCredentials {
|
|
5
|
+
organizationId: string;
|
|
6
|
+
apiToken: string;
|
|
7
|
+
userId?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parse Bearer token containing Productive credentials
|
|
11
|
+
* Token format: base64(organizationId:apiToken) or base64(organizationId:apiToken:userId)
|
|
12
|
+
*
|
|
13
|
+
* @param authHeader - Authorization header value (e.g., "Bearer base64...")
|
|
14
|
+
* @returns Parsed credentials or null if invalid
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseAuthHeader(authHeader: string | undefined | null): ProductiveCredentials | null;
|
|
17
|
+
/**
|
|
18
|
+
* Create a Bearer token from Productive credentials
|
|
19
|
+
* Useful for documentation and testing
|
|
20
|
+
*
|
|
21
|
+
* @param credentials - Productive credentials
|
|
22
|
+
* @returns Base64 encoded token (without "Bearer " prefix)
|
|
23
|
+
*/
|
|
24
|
+
export declare function createAuthToken(credentials: ProductiveCredentials): string;
|
|
25
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,qBAAqB,GAAG,IAAI,CAkCnG;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,qBAAqB,GAAG,MAAM,CAM1E"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function parseAuthHeader(authHeader) {
|
|
2
|
+
if (!authHeader) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
6
|
+
if (!match) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const token = match[1];
|
|
10
|
+
try {
|
|
11
|
+
const decoded = Buffer.from(token, "base64").toString("utf-8");
|
|
12
|
+
const parts = decoded.split(":");
|
|
13
|
+
if (parts.length < 2) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const [organizationId, apiToken, userId] = parts;
|
|
17
|
+
if (!organizationId || !apiToken) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
organizationId,
|
|
22
|
+
apiToken,
|
|
23
|
+
userId: userId || void 0
|
|
24
|
+
};
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function createAuthToken(credentials) {
|
|
30
|
+
const parts = [credentials.organizationId, credentials.apiToken];
|
|
31
|
+
if (credentials.userId) {
|
|
32
|
+
parts.push(credentials.userId);
|
|
33
|
+
}
|
|
34
|
+
return Buffer.from(parts.join(":")).toString("base64");
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
createAuthToken,
|
|
38
|
+
parseAuthHeader
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sources":["../src/auth.ts"],"sourcesContent":["/**\n * Authentication utilities for Productive MCP server\n */\n\nexport interface ProductiveCredentials {\n organizationId: string;\n apiToken: string;\n userId?: string;\n}\n\n/**\n * Parse Bearer token containing Productive credentials\n * Token format: base64(organizationId:apiToken) or base64(organizationId:apiToken:userId)\n *\n * @param authHeader - Authorization header value (e.g., \"Bearer base64...\")\n * @returns Parsed credentials or null if invalid\n */\nexport function parseAuthHeader(authHeader: string | undefined | null): ProductiveCredentials | null {\n if (!authHeader) {\n return null;\n }\n\n const match = authHeader.match(/^Bearer\\s+(.+)$/i);\n if (!match) {\n return null;\n }\n\n const token = match[1];\n\n try {\n const decoded = Buffer.from(token, 'base64').toString('utf-8');\n const parts = decoded.split(':');\n\n if (parts.length < 2) {\n return null;\n }\n\n const [organizationId, apiToken, userId] = parts;\n\n if (!organizationId || !apiToken) {\n return null;\n }\n\n return {\n organizationId,\n apiToken,\n userId: userId || undefined,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Create a Bearer token from Productive credentials\n * Useful for documentation and testing\n *\n * @param credentials - Productive credentials\n * @returns Base64 encoded token (without \"Bearer \" prefix)\n */\nexport function createAuthToken(credentials: ProductiveCredentials): string {\n const parts = [credentials.organizationId, credentials.apiToken];\n if (credentials.userId) {\n parts.push(credentials.userId);\n }\n return Buffer.from(parts.join(':')).toString('base64');\n}\n"],"names":[],"mappings":"AAiBO,SAAS,gBAAgB,YAAqE;AACnG,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,WAAW,MAAM,kBAAkB;AACjD,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,MAAM,CAAC;AAErB,MAAI;AACF,UAAM,UAAU,OAAO,KAAK,OAAO,QAAQ,EAAE,SAAS,OAAO;AAC7D,UAAM,QAAQ,QAAQ,MAAM,GAAG;AAE/B,QAAI,MAAM,SAAS,GAAG;AACpB,aAAO;AAAA,IACT;AAEA,UAAM,CAAC,gBAAgB,UAAU,MAAM,IAAI;AAE3C,QAAI,CAAC,kBAAkB,CAAC,UAAU;AAChC,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,QAAQ,UAAU;AAAA,IAAA;AAAA,EAEtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASO,SAAS,gBAAgB,aAA4C;AAC1E,QAAM,QAAQ,CAAC,YAAY,gBAAgB,YAAY,QAAQ;AAC/D,MAAI,YAAY,QAAQ;AACtB,UAAM,KAAK,YAAY,MAAM;AAAA,EAC/B;AACA,SAAO,OAAO,KAAK,MAAM,KAAK,GAAG,CAAC,EAAE,SAAS,QAAQ;AACvD;"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response formatters for agent-friendly output
|
|
3
|
+
*
|
|
4
|
+
* This module re-exports formatters from @studiometa/productive-cli
|
|
5
|
+
* with MCP-specific defaults (no relationship IDs, no timestamps).
|
|
6
|
+
*/
|
|
7
|
+
import { type JsonApiResource, type JsonApiMeta, type FormatOptions, type FormattedPagination } from '@studiometa/productive-cli';
|
|
8
|
+
export type { JsonApiResource, JsonApiMeta, FormatOptions, FormattedPagination };
|
|
9
|
+
/**
|
|
10
|
+
* Format time entry for agent consumption
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatTimeEntry(entry: JsonApiResource, _included?: JsonApiResource[]): Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Format project for agent consumption
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatProject(project: JsonApiResource, _included?: JsonApiResource[]): Record<string, unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Format task for agent consumption
|
|
19
|
+
* Tasks use included resources to resolve project/company names
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatTask(task: JsonApiResource, included?: JsonApiResource[]): Record<string, unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Format person for agent consumption
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatPerson(person: JsonApiResource, _included?: JsonApiResource[]): Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* Format service for agent consumption
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatService(service: JsonApiResource, _included?: JsonApiResource[]): Record<string, unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* Format list response with pagination
|
|
32
|
+
*
|
|
33
|
+
* @param data - Array of JSON:API resources
|
|
34
|
+
* @param formatter - Formatter function (item, included?) => T
|
|
35
|
+
* @param meta - Pagination metadata
|
|
36
|
+
* @param included - Included resources for relationship resolution
|
|
37
|
+
*/
|
|
38
|
+
export declare function formatListResponse<T>(data: JsonApiResource[], formatter: (item: JsonApiResource, included?: JsonApiResource[]) => T, meta?: JsonApiMeta, included?: JsonApiResource[]): {
|
|
39
|
+
data: T[];
|
|
40
|
+
meta?: FormattedPagination;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=formatters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatters.d.ts","sourceRoot":"","sources":["../src/formatters.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAC;AAGpC,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,mBAAmB,EAAE,CAAC;AAcjF;;GAEG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,eAAe,EACtB,SAAS,CAAC,EAAE,eAAe,EAAE,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,eAAe,EACxB,SAAS,CAAC,EAAE,eAAe,EAAE,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,eAAe,EACrB,QAAQ,CAAC,EAAE,eAAe,EAAE,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,eAAe,EACvB,SAAS,CAAC,EAAE,eAAe,EAAE,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,eAAe,EACxB,SAAS,CAAC,EAAE,eAAe,EAAE,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,IAAI,EAAE,eAAe,EAAE,EACvB,SAAS,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,eAAe,EAAE,KAAK,CAAC,EACrE,IAAI,CAAC,EAAE,WAAW,EAClB,QAAQ,CAAC,EAAE,eAAe,EAAE,GAC3B;IAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,mBAAmB,CAAA;CAAE,CAY3C"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool execution handlers for Productive MCP server
|
|
3
|
+
* These are shared between stdio and HTTP transports
|
|
4
|
+
*/
|
|
5
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import type { ProductiveCredentials } from './auth.js';
|
|
7
|
+
export type ToolResult = CallToolResult;
|
|
8
|
+
/**
|
|
9
|
+
* Execute a tool with the given credentials and arguments
|
|
10
|
+
*
|
|
11
|
+
* @param name - Tool name
|
|
12
|
+
* @param args - Tool arguments
|
|
13
|
+
* @param credentials - Productive API credentials
|
|
14
|
+
* @returns Tool execution result
|
|
15
|
+
*/
|
|
16
|
+
export declare function executeToolWithCredentials(name: string, args: Record<string, unknown>, credentials: ProductiveCredentials): Promise<ToolResult>;
|
|
17
|
+
//# sourceMappingURL=handlers.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/handlers.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { formatPerson as formatPerson$1, formatListResponse as formatListResponse$1, formatService as formatService$1, formatTask as formatTask$1, formatTimeEntry as formatTimeEntry$1, formatProject as formatProject$1, ProductiveApi } from "@studiometa/productive-cli";
|
|
2
|
+
const MCP_FORMAT_OPTIONS = {
|
|
3
|
+
includeRelationshipIds: false,
|
|
4
|
+
includeTimestamps: false,
|
|
5
|
+
stripHtml: true
|
|
6
|
+
};
|
|
7
|
+
function formatTimeEntry(entry, _included) {
|
|
8
|
+
return formatTimeEntry$1(entry, MCP_FORMAT_OPTIONS);
|
|
9
|
+
}
|
|
10
|
+
function formatProject(project, _included) {
|
|
11
|
+
return formatProject$1(project, MCP_FORMAT_OPTIONS);
|
|
12
|
+
}
|
|
13
|
+
function formatTask(task, included) {
|
|
14
|
+
return formatTask$1(task, { ...MCP_FORMAT_OPTIONS, included });
|
|
15
|
+
}
|
|
16
|
+
function formatPerson(person, _included) {
|
|
17
|
+
return formatPerson$1(person, MCP_FORMAT_OPTIONS);
|
|
18
|
+
}
|
|
19
|
+
function formatService(service, _included) {
|
|
20
|
+
return formatService$1(service, MCP_FORMAT_OPTIONS);
|
|
21
|
+
}
|
|
22
|
+
function formatListResponse(data, formatter, meta, included) {
|
|
23
|
+
const wrappedFormatter = (item, _options) => {
|
|
24
|
+
return formatter(item, included);
|
|
25
|
+
};
|
|
26
|
+
const result = formatListResponse$1(data, wrappedFormatter, meta, {
|
|
27
|
+
...MCP_FORMAT_OPTIONS,
|
|
28
|
+
included
|
|
29
|
+
});
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function jsonResult(data) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function errorResult(message) {
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
40
|
+
isError: true
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function executeToolWithCredentials(name, args, credentials) {
|
|
44
|
+
const api = new ProductiveApi({
|
|
45
|
+
apiToken: credentials.apiToken,
|
|
46
|
+
organizationId: credentials.organizationId
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
switch (name) {
|
|
50
|
+
case "productive_list_projects": {
|
|
51
|
+
const result = await api.getProjects(args);
|
|
52
|
+
return jsonResult(formatListResponse(result.data, formatProject, result.meta));
|
|
53
|
+
}
|
|
54
|
+
case "productive_get_project": {
|
|
55
|
+
const result = await api.getProject(args.id);
|
|
56
|
+
return jsonResult(formatProject(result.data));
|
|
57
|
+
}
|
|
58
|
+
case "productive_list_time_entries": {
|
|
59
|
+
const result = await api.getTimeEntries(args);
|
|
60
|
+
return jsonResult(formatListResponse(result.data, formatTimeEntry, result.meta));
|
|
61
|
+
}
|
|
62
|
+
case "productive_get_time_entry": {
|
|
63
|
+
const result = await api.getTimeEntry(args.id);
|
|
64
|
+
return jsonResult(formatTimeEntry(result.data));
|
|
65
|
+
}
|
|
66
|
+
case "productive_create_time_entry": {
|
|
67
|
+
const result = await api.createTimeEntry(
|
|
68
|
+
args
|
|
69
|
+
);
|
|
70
|
+
return jsonResult({
|
|
71
|
+
success: true,
|
|
72
|
+
...formatTimeEntry(result.data)
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
case "productive_update_time_entry": {
|
|
76
|
+
const { id, ...data } = args;
|
|
77
|
+
const result = await api.updateTimeEntry(id, data);
|
|
78
|
+
return jsonResult({
|
|
79
|
+
success: true,
|
|
80
|
+
...formatTimeEntry(result.data)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
case "productive_delete_time_entry": {
|
|
84
|
+
await api.deleteTimeEntry(args.id);
|
|
85
|
+
return jsonResult({ success: true, message: "Time entry deleted" });
|
|
86
|
+
}
|
|
87
|
+
case "productive_list_tasks": {
|
|
88
|
+
const params = args || {};
|
|
89
|
+
params.include = ["project", "project.company"];
|
|
90
|
+
const result = await api.getTasks(params);
|
|
91
|
+
return jsonResult(formatListResponse(
|
|
92
|
+
result.data,
|
|
93
|
+
formatTask,
|
|
94
|
+
result.meta,
|
|
95
|
+
result.included
|
|
96
|
+
));
|
|
97
|
+
}
|
|
98
|
+
case "productive_get_task": {
|
|
99
|
+
const result = await api.getTask(args.id, {
|
|
100
|
+
include: ["project", "project.company"]
|
|
101
|
+
});
|
|
102
|
+
return jsonResult(formatTask(result.data, result.included));
|
|
103
|
+
}
|
|
104
|
+
case "productive_list_services": {
|
|
105
|
+
const result = await api.getServices(args);
|
|
106
|
+
return jsonResult(formatListResponse(result.data, formatService, result.meta));
|
|
107
|
+
}
|
|
108
|
+
case "productive_list_people": {
|
|
109
|
+
const result = await api.getPeople(args);
|
|
110
|
+
return jsonResult(formatListResponse(result.data, formatPerson, result.meta));
|
|
111
|
+
}
|
|
112
|
+
case "productive_get_person": {
|
|
113
|
+
const result = await api.getPerson(args.id);
|
|
114
|
+
return jsonResult(formatPerson(result.data));
|
|
115
|
+
}
|
|
116
|
+
case "productive_get_current_user": {
|
|
117
|
+
if (credentials.userId) {
|
|
118
|
+
const result = await api.getPerson(credentials.userId);
|
|
119
|
+
return jsonResult(formatPerson(result.data));
|
|
120
|
+
}
|
|
121
|
+
return jsonResult({
|
|
122
|
+
message: "User ID not configured. Set userId in credentials to use this tool.",
|
|
123
|
+
organizationId: credentials.organizationId
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
return errorResult(`Unknown tool: ${name}`);
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
return errorResult(message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
executeToolWithCredentials
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=handlers.js.map
|