@mkacki98/chestnut-mcp 0.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 +74 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +59 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +37 -0
- package/dist/oauth.d.ts +1 -0
- package/dist/oauth.js +60 -0
- package/dist/tools/create-pending-course.d.ts +27 -0
- package/dist/tools/create-pending-course.js +63 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Chestnut MCP Server
|
|
2
|
+
|
|
3
|
+
MCP server for integrating Claude Code with the Chestnut learning platform. Allows Claude Code to suggest relevant courses based on coding patterns.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @mkacki98/chestnut-mcp auth
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
### 1. Authenticate
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @mkacki98/chestnut-mcp auth
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This will:
|
|
20
|
+
- Open your browser to Chestnut
|
|
21
|
+
- Prompt you to log in (if not already logged in)
|
|
22
|
+
- Generate an access token
|
|
23
|
+
- Save the token locally
|
|
24
|
+
|
|
25
|
+
### 2. Configure Claude Code
|
|
26
|
+
|
|
27
|
+
Add to your Claude Code MCP settings:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"chestnut": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["@mkacki98/chestnut-mcp"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. Use Claude Code
|
|
41
|
+
|
|
42
|
+
Claude Code will now automatically suggest courses when it detects learning opportunities. Suggestions appear in your Chestnut dashboard.
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
- `npx chestnut-mcp auth` - Authenticate with Chestnut
|
|
47
|
+
- `npx chestnut-mcp status` - Check authentication status
|
|
48
|
+
- `npx chestnut-mcp logout` - Clear saved credentials
|
|
49
|
+
- `npx chestnut-mcp config` - View/set configuration
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Install dependencies
|
|
55
|
+
npm install
|
|
56
|
+
|
|
57
|
+
# Build
|
|
58
|
+
npm run build
|
|
59
|
+
|
|
60
|
+
# Test locally
|
|
61
|
+
npm run dev
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## How It Works
|
|
65
|
+
|
|
66
|
+
1. Claude Code detects you're using a suboptimal pattern
|
|
67
|
+
2. On session end, it calls the `create_pending_course` tool
|
|
68
|
+
3. Tool sends suggestion to your Chestnut account
|
|
69
|
+
4. You see the suggestion in your dashboard
|
|
70
|
+
5. Click "Start Course" to begin learning
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { initiateOAuth } from "./oauth.js";
|
|
3
|
+
import { loadAccessToken, clearAccessToken, getChestnutUrl, setChestnutUrl } from "./config.js";
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
const command = args[0];
|
|
6
|
+
async function main() {
|
|
7
|
+
switch (command) {
|
|
8
|
+
case "auth":
|
|
9
|
+
await initiateOAuth();
|
|
10
|
+
break;
|
|
11
|
+
case "status":
|
|
12
|
+
const token = loadAccessToken();
|
|
13
|
+
if (token) {
|
|
14
|
+
console.log("✓ Authenticated with Chestnut");
|
|
15
|
+
console.log(` Chestnut URL: ${getChestnutUrl()}`);
|
|
16
|
+
console.log(` Token: ${token.substring(0, 8)}...`);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.log("✗ Not authenticated");
|
|
20
|
+
console.log(" Run: npx chestnut-mcp auth");
|
|
21
|
+
}
|
|
22
|
+
break;
|
|
23
|
+
case "logout":
|
|
24
|
+
clearAccessToken();
|
|
25
|
+
console.log("✓ Logged out successfully");
|
|
26
|
+
break;
|
|
27
|
+
case "config":
|
|
28
|
+
if (args[1] === "set-url" && args[2]) {
|
|
29
|
+
setChestnutUrl(args[2]);
|
|
30
|
+
console.log(`✓ Chestnut URL set to: ${args[2]}`);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
console.log(`Current Chestnut URL: ${getChestnutUrl()}`);
|
|
34
|
+
console.log("\nTo change URL:");
|
|
35
|
+
console.log(" npx chestnut-mcp config set-url https://your-url.com");
|
|
36
|
+
}
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
console.log("Chestnut MCP Server");
|
|
40
|
+
console.log("\nCommands:");
|
|
41
|
+
console.log(" auth Authenticate with Chestnut");
|
|
42
|
+
console.log(" status Check authentication status");
|
|
43
|
+
console.log(" logout Clear saved token");
|
|
44
|
+
console.log(" config View/set configuration");
|
|
45
|
+
console.log("\nFor Claude Code integration, add to your config:");
|
|
46
|
+
console.log(JSON.stringify({
|
|
47
|
+
mcpServers: {
|
|
48
|
+
chestnut: {
|
|
49
|
+
command: "npx",
|
|
50
|
+
args: ["@mkacki98/chestnut-mcp"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}, null, 2));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
main().catch((error) => {
|
|
57
|
+
console.error("Error:", error);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface Config {
|
|
2
|
+
accessToken?: string;
|
|
3
|
+
chestnutUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function loadConfig(): Config;
|
|
6
|
+
export declare function saveConfig(config: Config): void;
|
|
7
|
+
export declare function loadAccessToken(): string | null;
|
|
8
|
+
export declare function saveAccessToken(token: string): void;
|
|
9
|
+
export declare function clearAccessToken(): void;
|
|
10
|
+
export declare function getChestnutUrl(): string;
|
|
11
|
+
export declare function setChestnutUrl(url: string): void;
|
|
12
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), ".chestnut-mcp");
|
|
5
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
6
|
+
export function loadConfig() {
|
|
7
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const content = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
12
|
+
return JSON.parse(content);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function saveConfig(config) {
|
|
19
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
20
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
23
|
+
}
|
|
24
|
+
export function loadAccessToken() {
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
return config.accessToken || null;
|
|
27
|
+
}
|
|
28
|
+
export function saveAccessToken(token) {
|
|
29
|
+
const config = loadConfig();
|
|
30
|
+
config.accessToken = token;
|
|
31
|
+
saveConfig(config);
|
|
32
|
+
}
|
|
33
|
+
export function clearAccessToken() {
|
|
34
|
+
const config = loadConfig();
|
|
35
|
+
delete config.accessToken;
|
|
36
|
+
saveConfig(config);
|
|
37
|
+
}
|
|
38
|
+
export function getChestnutUrl() {
|
|
39
|
+
const config = loadConfig();
|
|
40
|
+
return config.chestnutUrl || "https://chestnut.com"; // Replace with actual production URL
|
|
41
|
+
}
|
|
42
|
+
export function setChestnutUrl(url) {
|
|
43
|
+
const config = loadConfig();
|
|
44
|
+
config.chestnutUrl = url;
|
|
45
|
+
saveConfig(config);
|
|
46
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { createPendingCourseTool, executeCreatePendingCourse, } from "./tools/create-pending-course.js";
|
|
6
|
+
const server = new Server({
|
|
7
|
+
name: "chestnut-mcp",
|
|
8
|
+
version: "0.1.0",
|
|
9
|
+
}, {
|
|
10
|
+
capabilities: {
|
|
11
|
+
tools: {},
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
// List available tools
|
|
15
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
16
|
+
return {
|
|
17
|
+
tools: [createPendingCourseTool],
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
// Handle tool execution
|
|
21
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
22
|
+
if (request.params.name === "create_pending_course") {
|
|
23
|
+
const args = request.params.arguments;
|
|
24
|
+
return await executeCreatePendingCourse(args);
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
27
|
+
});
|
|
28
|
+
// Start server
|
|
29
|
+
async function main() {
|
|
30
|
+
const transport = new StdioServerTransport();
|
|
31
|
+
await server.connect(transport);
|
|
32
|
+
console.error("Chestnut MCP server running on stdio");
|
|
33
|
+
}
|
|
34
|
+
main().catch((error) => {
|
|
35
|
+
console.error("Fatal error in main():", error);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initiateOAuth(): Promise<void>;
|
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import open from "open";
|
|
2
|
+
import { saveAccessToken, getChestnutUrl } from "./config.js";
|
|
3
|
+
import readline from "readline";
|
|
4
|
+
export async function initiateOAuth() {
|
|
5
|
+
const chestnutUrl = getChestnutUrl();
|
|
6
|
+
const authUrl = `${chestnutUrl}/api/claude-code/oauth/authorize`;
|
|
7
|
+
console.log("\nOpening browser for authorization...");
|
|
8
|
+
console.log(`If browser doesn't open, visit: ${authUrl}\n`);
|
|
9
|
+
try {
|
|
10
|
+
await open(authUrl);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
console.log("Could not open browser automatically.");
|
|
14
|
+
}
|
|
15
|
+
console.log("After authorizing, you'll be redirected to the dashboard with a token in the URL.");
|
|
16
|
+
console.log("Copy the token from the URL (the part after 'token=').\n");
|
|
17
|
+
const token = await promptForToken();
|
|
18
|
+
if (!token) {
|
|
19
|
+
console.log("❌ No token provided. Authentication cancelled.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Validate token by making a test request
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(`${chestnutUrl}/api/claude-code/pending-courses`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${token}`,
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
query: "_test_connection",
|
|
32
|
+
reason: "Testing MCP connection",
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
if (response.ok || response.status === 400) {
|
|
36
|
+
// 400 is ok - it means token is valid but request validation failed (expected for test)
|
|
37
|
+
saveAccessToken(token);
|
|
38
|
+
console.log("✓ Authenticated successfully with Chestnut!");
|
|
39
|
+
console.log(`Token saved to: ${process.env.HOME}/.chestnut-mcp/config.json\n`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log("❌ Invalid token. Please try again.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.log("❌ Failed to validate token:", error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function promptForToken() {
|
|
50
|
+
const rl = readline.createInterface({
|
|
51
|
+
input: process.stdin,
|
|
52
|
+
output: process.stdout,
|
|
53
|
+
});
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
rl.question("Paste your token here: ", (answer) => {
|
|
56
|
+
rl.close();
|
|
57
|
+
resolve(answer.trim());
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const createPendingCourseTool: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: string;
|
|
6
|
+
properties: {
|
|
7
|
+
query: {
|
|
8
|
+
type: string;
|
|
9
|
+
description: string;
|
|
10
|
+
};
|
|
11
|
+
reason: {
|
|
12
|
+
type: string;
|
|
13
|
+
description: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
required: string[];
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export declare function executeCreatePendingCourse(args: {
|
|
20
|
+
query: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
content: Array<{
|
|
24
|
+
type: string;
|
|
25
|
+
text: string;
|
|
26
|
+
}>;
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { loadAccessToken, getChestnutUrl } from "../config.js";
|
|
2
|
+
export const createPendingCourseTool = {
|
|
3
|
+
name: "create_pending_course",
|
|
4
|
+
description: "Suggest a learning course to the user based on patterns observed in their code. Use this when you notice the user could benefit from learning a specific topic or pattern.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
query: {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "The course topic to suggest (e.g., 'server actions in nextjs', 'react hooks best practices'). Keep it concise and searchable.",
|
|
11
|
+
},
|
|
12
|
+
reason: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Why you're suggesting this course. Explain what pattern or issue you noticed that prompted this suggestion (e.g., 'user is using API route pattern instead of Next.js native server actions').",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
required: ["query", "reason"],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export async function executeCreatePendingCourse(args) {
|
|
21
|
+
const token = loadAccessToken();
|
|
22
|
+
const chestnutUrl = getChestnutUrl();
|
|
23
|
+
if (!token) {
|
|
24
|
+
throw new Error("Not authenticated with Chestnut. Please run: npx chestnut-mcp auth");
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(`${chestnutUrl}/api/claude-code/pending-courses`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${token}`,
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
query: args.query,
|
|
35
|
+
reason: args.reason,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
if (response.status === 401) {
|
|
40
|
+
throw new Error("Authentication failed. Please re-authenticate: npx chestnut-mcp auth");
|
|
41
|
+
}
|
|
42
|
+
if (response.status === 429) {
|
|
43
|
+
throw new Error("Too many suggestions. Please try again later.");
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Failed to create course suggestion: ${response.statusText}`);
|
|
46
|
+
}
|
|
47
|
+
const data = await response.json();
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: `✓ Course suggestion sent to Chestnut: "${args.query}"\n\nThe user will see this suggestion in their dashboard and can start the course when ready.`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof Error) {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
throw new Error("Failed to create course suggestion");
|
|
62
|
+
}
|
|
63
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mkacki98/chestnut-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Chestnut learning platform integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"chestnut-mcp": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc && node dist/index.js",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"learning",
|
|
22
|
+
"chestnut"
|
|
23
|
+
],
|
|
24
|
+
"author": "Mikolaj Kacki",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^0.5.0",
|
|
28
|
+
"open": "^10.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.0.0",
|
|
32
|
+
"typescript": "^5.3.0"
|
|
33
|
+
}
|
|
34
|
+
}
|