basecamp-mcp 1.0.4 → 1.1.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/README.md +34 -61
- package/dist/index.d.ts +0 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -41
- package/dist/index.js.map +1 -1
- package/dist/tools/auth.d.ts +3 -0
- package/dist/tools/auth.d.ts.map +1 -0
- package/dist/tools/auth.js +163 -0
- package/dist/tools/auth.js.map +1 -0
- package/dist/utils/auth.d.ts +0 -20
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +12 -48
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/contentOperations.d.ts +2 -2
- package/dist/utils/credentials.d.ts +9 -0
- package/dist/utils/credentials.d.ts.map +1 -0
- package/dist/utils/credentials.js +35 -0
- package/dist/utils/credentials.js.map +1 -0
- package/dist/utils/oauth.d.ts +16 -0
- package/dist/utils/oauth.d.ts.map +1 -0
- package/dist/utils/oauth.js +131 -0
- package/dist/utils/oauth.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,9 +6,13 @@ Model Context Protocol (MCP) server for Basecamp integration. Enables LLMs to in
|
|
|
6
6
|
|
|
7
7
|
The Basecamp MCP server requires Node.js 18+ and works with various MCP clients including Claude Code CLI, Claude Desktop, Cursor, VS Code, and others.
|
|
8
8
|
|
|
9
|
-
###
|
|
9
|
+
### Prerequisites
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
You need a Basecamp OAuth app. Register one at [37signals Launchpad](https://launchpad.37signals.com/integrations) with the redirect URI set to `http://localhost:7652/callback`.
|
|
12
|
+
|
|
13
|
+
### Installation
|
|
14
|
+
|
|
15
|
+
Add the MCP server to your client with your OAuth credentials:
|
|
12
16
|
|
|
13
17
|
```json
|
|
14
18
|
{
|
|
@@ -18,114 +22,83 @@ The baseline setup applies across most tools:
|
|
|
18
22
|
"args": ["-y", "basecamp-mcp@latest"],
|
|
19
23
|
"env": {
|
|
20
24
|
"BASECAMP_CLIENT_ID": "your_client_id",
|
|
21
|
-
"BASECAMP_CLIENT_SECRET": "your_client_secret"
|
|
22
|
-
"BASECAMP_REFRESH_TOKEN": "your_refresh_token",
|
|
23
|
-
"BASECAMP_USER_AGENT": "YourApp (your@email.com)",
|
|
24
|
-
"BASECAMP_ACCOUNT_ID": "account_id"
|
|
25
|
+
"BASECAMP_CLIENT_SECRET": "your_client_secret"
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
### Installation by Client
|
|
32
|
-
|
|
33
32
|
**Claude Code CLI:**
|
|
34
33
|
```bash
|
|
35
34
|
claude mcp add basecamp npx basecamp-mcp@latest \
|
|
36
35
|
-e BASECAMP_CLIENT_ID=your_client_id \
|
|
37
|
-
-e BASECAMP_CLIENT_SECRET=your_client_secret
|
|
38
|
-
-e BASECAMP_REFRESH_TOKEN=your_refresh_token \
|
|
39
|
-
-e BASECAMP_USER_AGENT="YourApp (your@email.com)" \
|
|
40
|
-
-e BASECAMP_ACCOUNT_ID=account_id
|
|
36
|
+
-e BASECAMP_CLIENT_SECRET=your_client_secret
|
|
41
37
|
```
|
|
42
38
|
|
|
43
|
-
**Claude Desktop:** Follow the MCP install guide using the
|
|
39
|
+
**Claude Desktop:** Follow the MCP install guide using the JSON config above.
|
|
44
40
|
|
|
45
|
-
**Cursor:**
|
|
41
|
+
**Cursor:** Add configuration through Settings → Tools & Integrations → New MCP Server.
|
|
46
42
|
|
|
47
|
-
**VS Code:**
|
|
43
|
+
**VS Code:**
|
|
48
44
|
```bash
|
|
49
45
|
code --add-mcp '{"name":"basecamp","command":"npx","args":["-y", "basecamp-mcp@latest"]}'
|
|
50
46
|
```
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
## Configuration
|
|
48
|
+
### Authentication
|
|
55
49
|
|
|
56
|
-
|
|
50
|
+
Once the MCP server is running, authenticate using the built-in login tool:
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* **`BASECAMP_ACCOUNT_ID`** — Your Basecamp account ID
|
|
52
|
+
1. Call `basecamp_login` — a browser window will open for Basecamp authorization
|
|
53
|
+
2. Authorize the app in your browser
|
|
54
|
+
3. If you have multiple Basecamp accounts, call `basecamp_login` again with the desired `account_id`
|
|
55
|
+
4. Done! Credentials are saved to `~/.config/basecamp-mcp/credentials.json`
|
|
63
56
|
|
|
64
|
-
|
|
57
|
+
Use `basecamp_whoami` to check who you're logged in as, and `basecamp_logout` to remove stored credentials.
|
|
65
58
|
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
"mcpServers": {
|
|
69
|
-
"basecamp": {
|
|
70
|
-
"command": "npx",
|
|
71
|
-
"args": ["-y", "basecamp-mcp@latest"],
|
|
72
|
-
"env": {
|
|
73
|
-
"BASECAMP_CLIENT_ID": "your_client_id",
|
|
74
|
-
"BASECAMP_CLIENT_SECRET": "your_client_secret",
|
|
75
|
-
"BASECAMP_REFRESH_TOKEN": "your_refresh_token",
|
|
76
|
-
"BASECAMP_USER_AGENT": "YourApp (your@email.com)",
|
|
77
|
-
"BASECAMP_ACCOUNT_ID": "account_id"
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## Usage
|
|
85
|
-
|
|
86
|
-
### Running the Server
|
|
59
|
+
## Configuration
|
|
87
60
|
|
|
88
|
-
|
|
89
|
-
npm start
|
|
90
|
-
```
|
|
61
|
+
The server requires these environment variables:
|
|
91
62
|
|
|
92
|
-
|
|
63
|
+
* **`BASECAMP_CLIENT_ID`** — Your Basecamp OAuth client ID
|
|
64
|
+
* **`BASECAMP_CLIENT_SECRET`** — Your Basecamp OAuth client secret
|
|
93
65
|
|
|
94
|
-
|
|
95
|
-
npm run dev
|
|
96
|
-
```
|
|
66
|
+
## Available Tools
|
|
97
67
|
|
|
98
|
-
###
|
|
68
|
+
### Authentication
|
|
69
|
+
- `basecamp_login` - Authenticate with Basecamp via OAuth browser flow
|
|
70
|
+
- `basecamp_logout` - Remove stored credentials
|
|
71
|
+
- `basecamp_whoami` - Show the currently authenticated user
|
|
99
72
|
|
|
100
|
-
|
|
73
|
+
### Projects
|
|
101
74
|
- `basecamp_list_projects` - List all accessible projects with optional filtering
|
|
102
75
|
- `basecamp_get_project` - Get detailed project information including dock configuration
|
|
103
76
|
|
|
104
|
-
|
|
77
|
+
### Messages
|
|
105
78
|
- `basecamp_list_messages` - List messages in a message board with optional filtering
|
|
106
79
|
- `basecamp_list_message_types` - List available message types/categories for a project
|
|
107
80
|
- `basecamp_get_message` - Get single message details
|
|
108
81
|
- `basecamp_create_message` - Create new message with optional category and draft status
|
|
109
82
|
- `basecamp_update_message` - Update message with advanced content editing (supports full replacement, append, prepend, search/replace)
|
|
110
83
|
|
|
111
|
-
|
|
84
|
+
### TODOs
|
|
112
85
|
- `basecamp_get_todoset` - Get todo set container with all todo lists
|
|
113
86
|
- `basecamp_list_todos` - List todos in a list with status filtering (active/archived)
|
|
114
87
|
- `basecamp_create_todo` - Create new todo with optional description
|
|
115
88
|
- `basecamp_complete_todo` - Mark todo as complete
|
|
116
89
|
- `basecamp_uncomplete_todo` - Mark todo as incomplete
|
|
117
90
|
|
|
118
|
-
|
|
91
|
+
### Comments
|
|
119
92
|
- `basecamp_list_comments` - List comments on any resource (works universally on all recording types)
|
|
120
93
|
- `basecamp_create_comment` - Add comment to any resource
|
|
121
94
|
- `basecamp_update_comment` - Update comment with advanced content editing (supports full replacement, append, prepend, search/replace)
|
|
122
95
|
|
|
123
|
-
|
|
96
|
+
### People
|
|
124
97
|
- `basecamp_get_me` - Get personal information for the authenticated user
|
|
125
98
|
- `basecamp_list_people` - List all people with optional filtering by name, email, or title
|
|
126
99
|
- `basecamp_get_person` - Get person details
|
|
127
100
|
|
|
128
|
-
|
|
101
|
+
### Kanban
|
|
129
102
|
- `basecamp_list_kanban_columns` - List all columns in a kanban board
|
|
130
103
|
- `basecamp_list_kanban_cards` - List cards in a column with steps and assignees
|
|
131
104
|
- `basecamp_get_kanban_card` - Get complete details of a specific card
|
|
@@ -133,7 +106,7 @@ npm run dev
|
|
|
133
106
|
- `basecamp_update_kanban_card` - Update card with advanced content editing (supports full replacement, append, prepend, search/replace, plus title, due date, assignees, notifications, and complete step array management)
|
|
134
107
|
- `basecamp_move_kanban_card` - Move a card to a different column and/or position
|
|
135
108
|
|
|
136
|
-
|
|
109
|
+
### Activity
|
|
137
110
|
- `basecamp_list_recordings` - Browse recent activity globally or across specific projects, with filtering by type, date range, person, and text search. All filters support multiple values for OR-matching (e.g., multiple project IDs, person IDs, types, or search terms)
|
|
138
111
|
- `basecamp_list_campfire_messages` - Browse chat messages from Campfires with filtering by campfire, person, text content, and date range. All filters support multiple values for OR-matching
|
|
139
112
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,21 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Basecamp MCP Server
|
|
4
|
-
*
|
|
5
|
-
* MCP server for interacting with Basecamp API, providing tools for:
|
|
6
|
-
* - Projects (discovering bucket IDs)
|
|
7
|
-
* - Messages (with patch support for updates)
|
|
8
|
-
* - TODOs (sets, lists, todos)
|
|
9
|
-
* - Comments (universal - work on any resource)
|
|
10
|
-
* - People
|
|
11
|
-
* - Kanban (cards, columns, steps)
|
|
12
|
-
* - Activity (recordings browsing)
|
|
13
|
-
*
|
|
14
|
-
* Environment variables required:
|
|
15
|
-
* - BASECAMP_CLIENT_ID
|
|
16
|
-
* - BASECAMP_CLIENT_SECRET
|
|
17
|
-
* - BASECAMP_REFRESH_TOKEN
|
|
18
|
-
* - BASECAMP_USER_AGENT
|
|
19
|
-
*/
|
|
20
2
|
export {};
|
|
21
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
CHANGED
|
@@ -1,58 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Basecamp MCP Server
|
|
4
|
-
*
|
|
5
|
-
* MCP server for interacting with Basecamp API, providing tools for:
|
|
6
|
-
* - Projects (discovering bucket IDs)
|
|
7
|
-
* - Messages (with patch support for updates)
|
|
8
|
-
* - TODOs (sets, lists, todos)
|
|
9
|
-
* - Comments (universal - work on any resource)
|
|
10
|
-
* - People
|
|
11
|
-
* - Kanban (cards, columns, steps)
|
|
12
|
-
* - Activity (recordings browsing)
|
|
13
|
-
*
|
|
14
|
-
* Environment variables required:
|
|
15
|
-
* - BASECAMP_CLIENT_ID
|
|
16
|
-
* - BASECAMP_CLIENT_SECRET
|
|
17
|
-
* - BASECAMP_REFRESH_TOKEN
|
|
18
|
-
* - BASECAMP_USER_AGENT
|
|
19
|
-
*/
|
|
20
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
4
|
import { registerActivityTools } from "./tools/activity.js";
|
|
5
|
+
import { registerAuthTools } from "./tools/auth.js";
|
|
23
6
|
import { registerCommentTools } from "./tools/comments.js";
|
|
24
7
|
import { registerDocumentTools } from "./tools/documents.js";
|
|
25
8
|
import { registerKanbanTools } from "./tools/kanban.js";
|
|
26
9
|
import { registerMessageTools } from "./tools/messages.js";
|
|
27
10
|
import { registerPeopleTools } from "./tools/people.js";
|
|
28
|
-
// Import tool registration functions
|
|
29
11
|
import { registerProjectTools } from "./tools/projects.js";
|
|
30
12
|
import { registerTodoTools } from "./tools/todos.js";
|
|
31
13
|
import { registerUploadTools } from "./tools/uploads.js";
|
|
32
|
-
/**
|
|
33
|
-
* Main server initialization and startup
|
|
34
|
-
*/
|
|
35
14
|
async function main() {
|
|
36
|
-
// Validate required environment variables
|
|
37
|
-
const requiredEnvVars = [
|
|
38
|
-
"BASECAMP_CLIENT_ID",
|
|
39
|
-
"BASECAMP_CLIENT_SECRET",
|
|
40
|
-
"BASECAMP_REFRESH_TOKEN",
|
|
41
|
-
"BASECAMP_USER_AGENT",
|
|
42
|
-
];
|
|
43
|
-
const missing = requiredEnvVars.filter((varName) => !process.env[varName]);
|
|
44
|
-
if (missing.length > 0) {
|
|
45
|
-
console.error(`ERROR: Missing required environment variables: ${missing.join(", ")}`);
|
|
46
|
-
console.error("Please set these in your environment or .env file before starting the server.");
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
// Create MCP server instance
|
|
50
15
|
const server = new McpServer({
|
|
51
16
|
name: "basecamp-mcp-server",
|
|
52
17
|
version: "1.0.0",
|
|
53
18
|
});
|
|
54
|
-
// Register all tool categories
|
|
55
19
|
console.error("Registering tools...");
|
|
20
|
+
registerAuthTools(server);
|
|
56
21
|
registerProjectTools(server);
|
|
57
22
|
registerMessageTools(server);
|
|
58
23
|
registerTodoTools(server);
|
|
@@ -63,14 +28,10 @@ async function main() {
|
|
|
63
28
|
registerDocumentTools(server);
|
|
64
29
|
registerUploadTools(server);
|
|
65
30
|
console.error("Tools registered successfully");
|
|
66
|
-
// Create stdio transport
|
|
67
31
|
const transport = new StdioServerTransport();
|
|
68
|
-
// Connect server to transport
|
|
69
32
|
await server.connect(transport);
|
|
70
33
|
console.error("Basecamp MCP server running on stdio");
|
|
71
|
-
console.error("Waiting for client connection...");
|
|
72
34
|
}
|
|
73
|
-
// Run the server
|
|
74
35
|
main().catch((error) => {
|
|
75
36
|
console.error("Fatal error starting Basecamp MCP server:", error);
|
|
76
37
|
process.exit(1);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD,KAAK,UAAU,IAAI;IAClB,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC5B,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,OAAO;KAChB,CAAC,CAAC;IAEH,OAAO,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;IACtC,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,OAAO,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAE/C,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,OAAO,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;AACvD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACtB,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,KAAK,CAAC,CAAC;IAClE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/tools/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAYzE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA2LzD"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { initializeBasecampClient, clearTokenCache } from "../utils/auth.js";
|
|
3
|
+
import { deleteCredentials, getCredentialsPath, readCredentials, writeCredentials, } from "../utils/credentials.js";
|
|
4
|
+
import { handleBasecampError } from "../utils/errorHandlers.js";
|
|
5
|
+
import { fetchBasecampAccounts, performBasecampOAuthLogin } from "../utils/oauth.js";
|
|
6
|
+
export function registerAuthTools(server) {
|
|
7
|
+
server.registerTool("basecamp_login", {
|
|
8
|
+
title: "Login to Basecamp",
|
|
9
|
+
description: "Authenticate with Basecamp via OAuth. Opens a browser window for authorization. " +
|
|
10
|
+
"If you have multiple Basecamp accounts, call first without account_id to see the list, " +
|
|
11
|
+
"then call again with the desired account_id.",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
account_id: z
|
|
14
|
+
.number()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Basecamp account ID. If omitted and you have multiple accounts, returns the list to choose from."),
|
|
17
|
+
},
|
|
18
|
+
annotations: {
|
|
19
|
+
readOnlyHint: false,
|
|
20
|
+
destructiveHint: false,
|
|
21
|
+
idempotentHint: true,
|
|
22
|
+
openWorldHint: true,
|
|
23
|
+
},
|
|
24
|
+
}, async (params) => {
|
|
25
|
+
try {
|
|
26
|
+
const { accessToken, refreshToken } = await performBasecampOAuthLogin();
|
|
27
|
+
const accounts = await fetchBasecampAccounts(accessToken);
|
|
28
|
+
if (accounts.length === 0) {
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: "text",
|
|
33
|
+
text: "No Basecamp 3 accounts found for this user.",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
let accountId;
|
|
39
|
+
if (params.account_id) {
|
|
40
|
+
const match = accounts.find((a) => a.id === params.account_id);
|
|
41
|
+
if (!match) {
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text",
|
|
46
|
+
text: `Account ID ${params.account_id} not found. Available accounts:\n${formatAccountList(accounts)}`,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
accountId = String(match.id);
|
|
52
|
+
}
|
|
53
|
+
else if (accounts.length === 1) {
|
|
54
|
+
accountId = String(accounts[0].id);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `Multiple Basecamp accounts found. Please call basecamp_login again with account_id set to one of:\n${formatAccountList(accounts)}`,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
clearTokenCache();
|
|
67
|
+
await writeCredentials({ refreshToken, accountId });
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: `Login successful! Credentials saved to ${getCredentialsPath()}. Account ID: ${accountId}.`,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: "text",
|
|
82
|
+
text: `Login failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
server.registerTool("basecamp_logout", {
|
|
89
|
+
title: "Logout from Basecamp",
|
|
90
|
+
description: "Remove stored Basecamp credentials.",
|
|
91
|
+
annotations: {
|
|
92
|
+
readOnlyHint: false,
|
|
93
|
+
destructiveHint: true,
|
|
94
|
+
idempotentHint: true,
|
|
95
|
+
openWorldHint: false,
|
|
96
|
+
},
|
|
97
|
+
}, async () => {
|
|
98
|
+
clearTokenCache();
|
|
99
|
+
await deleteCredentials();
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: "text",
|
|
104
|
+
text: `Logged out. Credentials removed from ${getCredentialsPath()}.`,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
server.registerTool("basecamp_whoami", {
|
|
110
|
+
title: "Who Am I (Basecamp)",
|
|
111
|
+
description: "Show the currently authenticated Basecamp user, or indicate if not logged in.",
|
|
112
|
+
annotations: {
|
|
113
|
+
readOnlyHint: true,
|
|
114
|
+
destructiveHint: false,
|
|
115
|
+
idempotentHint: true,
|
|
116
|
+
openWorldHint: false,
|
|
117
|
+
},
|
|
118
|
+
}, async () => {
|
|
119
|
+
const creds = await readCredentials();
|
|
120
|
+
if (!creds) {
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: "text",
|
|
125
|
+
text: "Not logged in. Use basecamp_login to authenticate.",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const client = await initializeBasecampClient();
|
|
132
|
+
const response = await client.people.me({});
|
|
133
|
+
if (response.status !== 200 || !response.body) {
|
|
134
|
+
throw new Error("Failed to get profile");
|
|
135
|
+
}
|
|
136
|
+
const me = response.body;
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: JSON.stringify({
|
|
142
|
+
name: me.name,
|
|
143
|
+
email: me.email_address,
|
|
144
|
+
title: me.title,
|
|
145
|
+
account_id: creds.accountId,
|
|
146
|
+
}, null, 2),
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{ type: "text", text: handleBasecampError(error) },
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function formatAccountList(accounts) {
|
|
161
|
+
return accounts.map((a) => `- ${a.name} (id: ${a.id})`).join("\n");
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/tools/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/auth.d.ts
CHANGED
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authentication utilities for Basecamp API
|
|
3
|
-
*/
|
|
4
1
|
import { type Client } from "basecamp-client";
|
|
5
|
-
/**
|
|
6
|
-
* Initialize and return an authenticated Basecamp client.
|
|
7
|
-
*
|
|
8
|
-
* Uses environment variables:
|
|
9
|
-
* - BASECAMP_CLIENT_ID
|
|
10
|
-
* - BASECAMP_CLIENT_SECRET
|
|
11
|
-
* - BASECAMP_REFRESH_TOKEN
|
|
12
|
-
* - BASECAMP_USER_AGENT (optional)
|
|
13
|
-
* - BASECAMP_ACCOUNT_ID
|
|
14
|
-
*
|
|
15
|
-
* @param accountId - Basecamp account ID to use for the client
|
|
16
|
-
* @returns Authenticated Basecamp client instance
|
|
17
|
-
* @throws Error if required environment variables are missing or authentication fails
|
|
18
|
-
*/
|
|
19
2
|
export declare function initializeBasecampClient(): Promise<Client>;
|
|
20
|
-
/**
|
|
21
|
-
* Clear the cached bearer token (useful for forcing token refresh)
|
|
22
|
-
*/
|
|
23
3
|
export declare function clearTokenCache(): void;
|
|
24
4
|
//# sourceMappingURL=auth.d.ts.map
|
package/dist/utils/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/utils/auth.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/utils/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,MAAM,EAAkB,MAAM,iBAAiB,CAAC;AAM3E,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,MAAM,CAAC,CAqBhE;AAED,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
|
package/dist/utils/auth.js
CHANGED
|
@@ -1,61 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authentication utilities for Basecamp API
|
|
3
|
-
*/
|
|
4
1
|
import { buildClient, getBearerToken } from "basecamp-client";
|
|
5
|
-
|
|
2
|
+
import { readCredentials } from "./credentials.js";
|
|
3
|
+
import { getClientCredentials } from "./oauth.js";
|
|
6
4
|
let cachedBearerToken = null;
|
|
7
|
-
/**
|
|
8
|
-
* Initialize and return an authenticated Basecamp client.
|
|
9
|
-
*
|
|
10
|
-
* Uses environment variables:
|
|
11
|
-
* - BASECAMP_CLIENT_ID
|
|
12
|
-
* - BASECAMP_CLIENT_SECRET
|
|
13
|
-
* - BASECAMP_REFRESH_TOKEN
|
|
14
|
-
* - BASECAMP_USER_AGENT (optional)
|
|
15
|
-
* - BASECAMP_ACCOUNT_ID
|
|
16
|
-
*
|
|
17
|
-
* @param accountId - Basecamp account ID to use for the client
|
|
18
|
-
* @returns Authenticated Basecamp client instance
|
|
19
|
-
* @throws Error if required environment variables are missing or authentication fails
|
|
20
|
-
*/
|
|
21
5
|
export async function initializeBasecampClient() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"BASECAMP_CLIENT_SECRET",
|
|
26
|
-
"BASECAMP_REFRESH_TOKEN",
|
|
27
|
-
"BASECAMP_ACCOUNT_ID",
|
|
28
|
-
];
|
|
29
|
-
const missing = requiredEnvVars.filter((varName) => !process.env[varName]);
|
|
30
|
-
if (missing.length > 0) {
|
|
31
|
-
throw new Error(`Missing required environment variables: ${missing.join(", ")}. ` +
|
|
32
|
-
`Please set these in your environment or .env file.`);
|
|
6
|
+
const creds = await readCredentials();
|
|
7
|
+
if (!creds) {
|
|
8
|
+
throw new Error("Not logged in. Use basecamp_login to authenticate.");
|
|
33
9
|
}
|
|
34
|
-
|
|
10
|
+
const { clientId, clientSecret } = getClientCredentials();
|
|
35
11
|
if (!cachedBearerToken) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
userAgent: process.env.BASECAMP_USER_AGENT,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
throw new Error(`Failed to obtain Basecamp access token: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
46
|
-
`Check your BASECAMP_CLIENT_ID, BASECAMP_CLIENT_SECRET, and BASECAMP_REFRESH_TOKEN are correct.`);
|
|
47
|
-
}
|
|
12
|
+
cachedBearerToken = await getBearerToken({
|
|
13
|
+
clientId,
|
|
14
|
+
clientSecret,
|
|
15
|
+
refreshToken: creds.refreshToken,
|
|
16
|
+
});
|
|
48
17
|
}
|
|
49
|
-
// Build and return client
|
|
50
18
|
return buildClient({
|
|
51
19
|
bearerToken: cachedBearerToken,
|
|
52
|
-
accountId:
|
|
53
|
-
userAgent: process.env.BASECAMP_USER_AGENT,
|
|
20
|
+
accountId: creds.accountId,
|
|
54
21
|
});
|
|
55
22
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Clear the cached bearer token (useful for forcing token refresh)
|
|
58
|
-
*/
|
|
59
23
|
export function clearTokenCache() {
|
|
60
24
|
cachedBearerToken = null;
|
|
61
25
|
}
|
package/dist/utils/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/utils/auth.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/utils/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAe,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAElD,IAAI,iBAAiB,GAAkB,IAAI,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,wBAAwB;IAC7C,MAAM,KAAK,GAAG,MAAM,eAAe,EAAE,CAAC;IAEtC,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,oBAAoB,EAAE,CAAC;IAE1D,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACxB,iBAAiB,GAAG,MAAM,cAAc,CAAC;YACxC,QAAQ;YACR,YAAY;YACZ,YAAY,EAAE,KAAK,CAAC,YAAY;SAChC,CAAC,CAAC;IACJ,CAAC;IAED,OAAO,WAAW,CAAC;QAClB,WAAW,EAAE,iBAAiB;QAC9B,SAAS,EAAE,KAAK,CAAC,SAAS;KAC1B,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe;IAC9B,iBAAiB,GAAG,IAAI,CAAC;AAC1B,CAAC"}
|
|
@@ -16,11 +16,11 @@ export declare const ContentOperationFields: {
|
|
|
16
16
|
find: z.ZodString;
|
|
17
17
|
replace: z.ZodString;
|
|
18
18
|
}, "strip", z.ZodTypeAny, {
|
|
19
|
-
find: string;
|
|
20
19
|
replace: string;
|
|
21
|
-
}, {
|
|
22
20
|
find: string;
|
|
21
|
+
}, {
|
|
23
22
|
replace: string;
|
|
23
|
+
find: string;
|
|
24
24
|
}>, "many">>;
|
|
25
25
|
};
|
|
26
26
|
/**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type Credentials = {
|
|
2
|
+
refreshToken: string;
|
|
3
|
+
accountId: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function getCredentialsPath(): string;
|
|
6
|
+
export declare function readCredentials(): Promise<Credentials | null>;
|
|
7
|
+
export declare function writeCredentials(creds: Credentials): Promise<void>;
|
|
8
|
+
export declare function deleteCredentials(): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/utils/credentials.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,WAAW,GAAG;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CAClB,CAAC;AASF,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAanE;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxE;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMvD"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
const CREDENTIALS_PATH = join(homedir(), ".config", "basecamp-mcp", "credentials.json");
|
|
5
|
+
export function getCredentialsPath() {
|
|
6
|
+
return CREDENTIALS_PATH;
|
|
7
|
+
}
|
|
8
|
+
export async function readCredentials() {
|
|
9
|
+
try {
|
|
10
|
+
const content = await readFile(CREDENTIALS_PATH, "utf-8");
|
|
11
|
+
const data = JSON.parse(content);
|
|
12
|
+
if (!data.refreshToken || !data.accountId) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return { refreshToken: data.refreshToken, accountId: data.accountId };
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function writeCredentials(creds) {
|
|
22
|
+
const dir = dirname(CREDENTIALS_PATH);
|
|
23
|
+
await mkdir(dir, { recursive: true });
|
|
24
|
+
await writeFile(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), "utf-8");
|
|
25
|
+
await chmod(CREDENTIALS_PATH, 0o600);
|
|
26
|
+
}
|
|
27
|
+
export async function deleteCredentials() {
|
|
28
|
+
try {
|
|
29
|
+
await unlink(CREDENTIALS_PATH);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// File doesn't exist, nothing to do
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/utils/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAO1C,MAAM,gBAAgB,GAAG,IAAI,CAC5B,OAAO,EAAE,EACT,SAAS,EACT,cAAc,EACd,kBAAkB,CAClB,CAAC;AAEF,MAAM,UAAU,kBAAkB;IACjC,OAAO,gBAAgB,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe;IACpC,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAEjC,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YAC3C,OAAO,IAAI,CAAC;QACb,CAAC;QAED,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAAkB;IACxD,MAAM,GAAG,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACtC,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,MAAM,SAAS,CAAC,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC3E,MAAM,KAAK,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACtC,IAAI,CAAC;QACJ,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACR,oCAAoC;IACrC,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function getClientCredentials(): {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function performBasecampOAuthLogin(): Promise<{
|
|
6
|
+
accessToken: string;
|
|
7
|
+
refreshToken: string;
|
|
8
|
+
}>;
|
|
9
|
+
export type BasecampAccount = {
|
|
10
|
+
id: number;
|
|
11
|
+
name: string;
|
|
12
|
+
href: string;
|
|
13
|
+
product: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function fetchBasecampAccounts(accessToken: string): Promise<BasecampAccount[]>;
|
|
16
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/utils/oauth.ts"],"names":[],"mappings":"AAeA,wBAAgB,oBAAoB,IAAI;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACrB,CAWA;AAwID,wBAAsB,yBAAyB,IAAI,OAAO,CAAC;IAC1D,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACrB,CAAC,CAqBD;AAED,MAAM,MAAM,eAAe,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,qBAAqB,CAC1C,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,eAAe,EAAE,CAAC,CAiB5B"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { createServer, } from "node:http";
|
|
3
|
+
import { platform } from "node:os";
|
|
4
|
+
import { URL } from "node:url";
|
|
5
|
+
const REDIRECT_PORT = 7652;
|
|
6
|
+
const REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
|
|
7
|
+
const AUTHORIZE_BASE = "https://launchpad.37signals.com/authorization/new";
|
|
8
|
+
const TOKEN_URL = "https://launchpad.37signals.com/authorization/token";
|
|
9
|
+
const AUTH_INFO_URL = "https://launchpad.37signals.com/authorization.json";
|
|
10
|
+
export function getClientCredentials() {
|
|
11
|
+
const clientId = process.env.BASECAMP_CLIENT_ID;
|
|
12
|
+
const clientSecret = process.env.BASECAMP_CLIENT_SECRET;
|
|
13
|
+
if (!clientId || !clientSecret) {
|
|
14
|
+
throw new Error("Missing BASECAMP_CLIENT_ID and BASECAMP_CLIENT_SECRET environment variables.");
|
|
15
|
+
}
|
|
16
|
+
return { clientId, clientSecret };
|
|
17
|
+
}
|
|
18
|
+
function buildAuthorizeUrl(clientId) {
|
|
19
|
+
const params = new URLSearchParams({
|
|
20
|
+
type: "web_server",
|
|
21
|
+
client_id: clientId,
|
|
22
|
+
redirect_uri: REDIRECT_URI,
|
|
23
|
+
});
|
|
24
|
+
return `${AUTHORIZE_BASE}?${params.toString()}`;
|
|
25
|
+
}
|
|
26
|
+
function openBrowser(url) {
|
|
27
|
+
const os = platform();
|
|
28
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
29
|
+
exec(`${cmd} "${url}"`);
|
|
30
|
+
}
|
|
31
|
+
async function exchangeCodeForTokens(code, clientId, clientSecret) {
|
|
32
|
+
const response = await fetch(TOKEN_URL, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
type: "web_server",
|
|
37
|
+
client_id: clientId,
|
|
38
|
+
client_secret: clientSecret,
|
|
39
|
+
redirect_uri: REDIRECT_URI,
|
|
40
|
+
code,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const body = await response.text();
|
|
45
|
+
throw new Error(`Token exchange failed (${response.status}): ${body}`);
|
|
46
|
+
}
|
|
47
|
+
const data = (await response.json());
|
|
48
|
+
return { accessToken: data.access_token, refreshToken: data.refresh_token };
|
|
49
|
+
}
|
|
50
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
51
|
+
<html>
|
|
52
|
+
<head><title>Basecamp MCP</title></head>
|
|
53
|
+
<body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
|
|
54
|
+
<div style="text-align: center;">
|
|
55
|
+
<h1>Login successful!</h1>
|
|
56
|
+
<p>You can close this tab and return to your MCP client.</p>
|
|
57
|
+
</div>
|
|
58
|
+
</body>
|
|
59
|
+
</html>`;
|
|
60
|
+
function startCallbackServer(clientId, clientSecret) {
|
|
61
|
+
return new Promise((resolveStart) => {
|
|
62
|
+
let resolveTokens;
|
|
63
|
+
const tokensPromise = new Promise((resolve) => {
|
|
64
|
+
resolveTokens = resolve;
|
|
65
|
+
});
|
|
66
|
+
const server = createServer(async (req, res) => {
|
|
67
|
+
const url = new URL(req.url || "/", `http://localhost:${REDIRECT_PORT}`);
|
|
68
|
+
if (url.pathname !== "/callback") {
|
|
69
|
+
res.writeHead(404);
|
|
70
|
+
res.end("Not found");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const code = url.searchParams.get("code");
|
|
74
|
+
if (!code) {
|
|
75
|
+
const error = url.searchParams.get("error_description") ||
|
|
76
|
+
"No authorization code received";
|
|
77
|
+
res.writeHead(400);
|
|
78
|
+
res.end(error);
|
|
79
|
+
server.close();
|
|
80
|
+
resolveTokens(null);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const tokens = await exchangeCodeForTokens(code, clientId, clientSecret);
|
|
85
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
86
|
+
res.end(SUCCESS_HTML);
|
|
87
|
+
server.close();
|
|
88
|
+
resolveTokens(tokens);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
res.writeHead(500);
|
|
92
|
+
res.end(`Token exchange failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
93
|
+
server.close();
|
|
94
|
+
resolveTokens(null);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
server.once("error", () => {
|
|
98
|
+
resolveStart(null);
|
|
99
|
+
});
|
|
100
|
+
server.once("listening", () => {
|
|
101
|
+
resolveStart({ waitForTokens: () => tokensPromise });
|
|
102
|
+
});
|
|
103
|
+
server.listen(REDIRECT_PORT, "127.0.0.1");
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export async function performBasecampOAuthLogin() {
|
|
107
|
+
const { clientId, clientSecret } = getClientCredentials();
|
|
108
|
+
const serverHandle = await startCallbackServer(clientId, clientSecret);
|
|
109
|
+
if (!serverHandle) {
|
|
110
|
+
throw new Error(`Could not start local server on port ${REDIRECT_PORT}. Is another process using it?`);
|
|
111
|
+
}
|
|
112
|
+
const authorizeUrl = buildAuthorizeUrl(clientId);
|
|
113
|
+
openBrowser(authorizeUrl);
|
|
114
|
+
const tokens = await serverHandle.waitForTokens();
|
|
115
|
+
if (!tokens) {
|
|
116
|
+
throw new Error("Authentication failed — no tokens received");
|
|
117
|
+
}
|
|
118
|
+
return tokens;
|
|
119
|
+
}
|
|
120
|
+
export async function fetchBasecampAccounts(accessToken) {
|
|
121
|
+
const response = await fetch(AUTH_INFO_URL, {
|
|
122
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
123
|
+
});
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
const body = await response.text();
|
|
126
|
+
throw new Error(`Failed to fetch accounts (${response.status}): ${body}`);
|
|
127
|
+
}
|
|
128
|
+
const data = (await response.json());
|
|
129
|
+
return data.accounts.filter((a) => a.product === "bc3");
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/utils/oauth.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "basecamp-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for Basecamp integration. Enables LLMs to interact with Basecamp projects, messages, todos, comments, people, and kanban boards.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|