in10x-mcp 1.0.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 +103 -0
- package/bin/install.js +185 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +29 -0
- package/build/lib/contentGenerator.d.ts +10 -0
- package/build/lib/contentGenerator.js +37 -0
- package/build/lib/storage.d.ts +17 -0
- package/build/lib/storage.js +59 -0
- package/build/lib/types.d.ts +30 -0
- package/build/lib/types.js +1 -0
- package/build/tools/connectAccount.d.ts +6 -0
- package/build/tools/connectAccount.js +127 -0
- package/build/tools/generateContent.d.ts +8 -0
- package/build/tools/generateContent.js +97 -0
- package/build/tools/getContentHistory.d.ts +2 -0
- package/build/tools/getContentHistory.js +26 -0
- package/build/tools/pushToIn10x.d.ts +9 -0
- package/build/tools/pushToIn10x.js +147 -0
- package/build/tools/quickPost.d.ts +7 -0
- package/build/tools/quickPost.js +61 -0
- package/build/tools/setProjectContext.d.ts +9 -0
- package/build/tools/setProjectContext.js +46 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# In10x MCP
|
|
2
|
+
|
|
3
|
+
Turn boring coding sessions into interesting social content. An MCP server that generates engaging posts for X (Twitter) and the In10x platform.
|
|
4
|
+
|
|
5
|
+
## Quick Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx in10x-mcp@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The installer will:
|
|
12
|
+
1. Ask which editor you use (Claude Desktop, Cursor, or Windsurf)
|
|
13
|
+
2. Ask for your In10x token (get one at https://in10x.com/connect/mcp)
|
|
14
|
+
3. Configure the MCP automatically (preserving your existing MCPs)
|
|
15
|
+
4. Save your token so you're ready to post
|
|
16
|
+
|
|
17
|
+
Then restart your editor!
|
|
18
|
+
|
|
19
|
+
## Manual Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install
|
|
23
|
+
npm run build
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Add to your editor's MCP config:
|
|
27
|
+
|
|
28
|
+
| Editor | Config Path |
|
|
29
|
+
|--------|-------------|
|
|
30
|
+
| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
31
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
32
|
+
| Windsurf | `~/.windsurf/mcp.json` |
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"in10x": {
|
|
38
|
+
"command": "node",
|
|
39
|
+
"args": ["/path/to/in10x/build/index.js"]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Replace `/path/to/in10x` with the actual path to this directory.
|
|
46
|
+
|
|
47
|
+
## Tools
|
|
48
|
+
|
|
49
|
+
### generate_content
|
|
50
|
+
|
|
51
|
+
Generate engaging social content from your coding session.
|
|
52
|
+
|
|
53
|
+
**Parameters:**
|
|
54
|
+
- `session_summary` (optional) - What was built or worked on
|
|
55
|
+
- `tone` (optional) - "casual" | "professional" | "hype" (default: casual)
|
|
56
|
+
- `struggle` (optional) - What was hard or surprising
|
|
57
|
+
|
|
58
|
+
**Example:**
|
|
59
|
+
```
|
|
60
|
+
generate_content with session_summary="Built PDF export after trying 4 libraries" and struggle="jsPDF didn't support custom fonts"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### set_project_context
|
|
64
|
+
|
|
65
|
+
Store project info for better content generation. Set once at session start.
|
|
66
|
+
|
|
67
|
+
**Parameters:**
|
|
68
|
+
- `project_name` (required) - Name of your project
|
|
69
|
+
- `project_description` (optional) - One-liner description
|
|
70
|
+
- `stack` (optional) - Tech stack (e.g., "Next.js + Supabase")
|
|
71
|
+
- `started_at` (optional) - When project started
|
|
72
|
+
|
|
73
|
+
### get_content_history
|
|
74
|
+
|
|
75
|
+
View the last 5 generated posts from this session. No parameters.
|
|
76
|
+
|
|
77
|
+
### quick_post
|
|
78
|
+
|
|
79
|
+
Generate a quick post from a short update.
|
|
80
|
+
|
|
81
|
+
**Parameters:**
|
|
82
|
+
- `update` (required) - Quick update (e.g., "shipped payments")
|
|
83
|
+
- `tone` (optional) - "casual" | "professional" | "hype"
|
|
84
|
+
|
|
85
|
+
## Content Transformation
|
|
86
|
+
|
|
87
|
+
The MCP transforms boring updates into engaging content:
|
|
88
|
+
|
|
89
|
+
| Boring | Interesting |
|
|
90
|
+
|--------|-------------|
|
|
91
|
+
| "Fixed authentication bug" | "3 hours on auth. 20 minutes on payments. The hard part is never what you expect." |
|
|
92
|
+
| "Added PDF export" | "Cracked PDF export after trying 4 libraries. Claude one-shotted it with vanilla JS." |
|
|
93
|
+
| "Working on landing page" | "Shipped landing page in 2 hours. No Figma, no designer, just Claude + Tailwind." |
|
|
94
|
+
|
|
95
|
+
## Tone Options
|
|
96
|
+
|
|
97
|
+
- **casual** (default): Relatable, 1-2 emojis, conversational
|
|
98
|
+
- **professional**: Outcomes-focused, minimal emoji, credible
|
|
99
|
+
- **hype**: High energy, more emojis, celebratory
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import * as readline from "readline";
|
|
7
|
+
|
|
8
|
+
const EDITORS = {
|
|
9
|
+
1: {
|
|
10
|
+
name: "Claude Desktop",
|
|
11
|
+
configPath: path.join(
|
|
12
|
+
os.homedir(),
|
|
13
|
+
"Library/Application Support/Claude/claude_desktop_config.json"
|
|
14
|
+
),
|
|
15
|
+
},
|
|
16
|
+
2: {
|
|
17
|
+
name: "Cursor",
|
|
18
|
+
configPath: path.join(os.homedir(), ".cursor/mcp.json"),
|
|
19
|
+
},
|
|
20
|
+
3: {
|
|
21
|
+
name: "Windsurf",
|
|
22
|
+
configPath: path.join(os.homedir(), ".windsurf/mcp.json"),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const TOKEN_DIR = path.join(os.homedir(), ".in10x");
|
|
27
|
+
const TOKEN_FILE = path.join(TOKEN_DIR, "token");
|
|
28
|
+
|
|
29
|
+
function createReadline() {
|
|
30
|
+
return readline.createInterface({
|
|
31
|
+
input: process.stdin,
|
|
32
|
+
output: process.stdout,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function question(rl, prompt) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
rl.question(prompt, resolve);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function detectNodePath() {
|
|
43
|
+
// Check for Homebrew Node 20 first (common on Mac)
|
|
44
|
+
const homebrewNode20 = "/opt/homebrew/opt/node@20/bin/node";
|
|
45
|
+
if (fs.existsSync(homebrewNode20)) {
|
|
46
|
+
return homebrewNode20;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check for Homebrew Node (latest)
|
|
50
|
+
const homebrewNode = "/opt/homebrew/bin/node";
|
|
51
|
+
if (fs.existsSync(homebrewNode)) {
|
|
52
|
+
return homebrewNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fallback to system node
|
|
56
|
+
return "node";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getMcpPath() {
|
|
60
|
+
// The MCP build directory - resolve from this script's location
|
|
61
|
+
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
|
62
|
+
const mcpPath = path.join(scriptDir, "..", "build", "index.js");
|
|
63
|
+
return path.resolve(mcpPath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readConfig(configPath) {
|
|
67
|
+
if (!fs.existsSync(configPath)) {
|
|
68
|
+
return { mcpServers: {} };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
73
|
+
return JSON.parse(content);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// If file exists but is empty or invalid JSON, start fresh
|
|
76
|
+
return { mcpServers: {} };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeConfig(configPath, config) {
|
|
81
|
+
const dir = path.dirname(configPath);
|
|
82
|
+
if (!fs.existsSync(dir)) {
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function saveToken(token) {
|
|
89
|
+
if (!fs.existsSync(TOKEN_DIR)) {
|
|
90
|
+
fs.mkdirSync(TOKEN_DIR, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
fs.writeFileSync(TOKEN_FILE, token, "utf-8");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function main() {
|
|
96
|
+
console.log("\n in10x MCP Installer\n");
|
|
97
|
+
console.log(" Turn coding sessions into social content.\n");
|
|
98
|
+
|
|
99
|
+
const rl = createReadline();
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Step 1: Choose editor
|
|
103
|
+
console.log(" Which editor are you using?\n");
|
|
104
|
+
console.log(" 1) Claude Desktop");
|
|
105
|
+
console.log(" 2) Cursor");
|
|
106
|
+
console.log(" 3) Windsurf\n");
|
|
107
|
+
|
|
108
|
+
const editorChoice = await question(rl, " Enter choice (1-3): ");
|
|
109
|
+
const editorNum = parseInt(editorChoice.trim(), 10);
|
|
110
|
+
|
|
111
|
+
if (!EDITORS[editorNum]) {
|
|
112
|
+
console.log("\n Invalid choice. Exiting.\n");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const editor = EDITORS[editorNum];
|
|
117
|
+
console.log(`\n Selected: ${editor.name}`);
|
|
118
|
+
|
|
119
|
+
// Step 2: Get token
|
|
120
|
+
console.log("\n Get your token at: https://www.in10x.com/connect/mcp\n");
|
|
121
|
+
const token = await question(rl, " Paste your In10x token: ");
|
|
122
|
+
|
|
123
|
+
if (!token.trim()) {
|
|
124
|
+
console.log("\n No token provided. Exiting.\n");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Step 3: Detect node path
|
|
129
|
+
const nodePath = detectNodePath();
|
|
130
|
+
console.log(`\n Detected node: ${nodePath}`);
|
|
131
|
+
|
|
132
|
+
// Step 4: Get MCP path
|
|
133
|
+
const mcpPath = getMcpPath();
|
|
134
|
+
console.log(` MCP path: ${mcpPath}`);
|
|
135
|
+
|
|
136
|
+
// Step 5: Read existing config (preserve other MCPs)
|
|
137
|
+
const config = readConfig(editor.configPath);
|
|
138
|
+
console.log(`\n Reading config: ${editor.configPath}`);
|
|
139
|
+
|
|
140
|
+
if (!config.mcpServers) {
|
|
141
|
+
config.mcpServers = {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check if in10x already exists
|
|
145
|
+
if (config.mcpServers.in10x) {
|
|
146
|
+
console.log(" Updating existing in10x configuration...");
|
|
147
|
+
} else {
|
|
148
|
+
console.log(" Adding in10x to mcpServers...");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Step 6: Add in10x config with PATH for reliability
|
|
152
|
+
config.mcpServers.in10x = {
|
|
153
|
+
command: nodePath,
|
|
154
|
+
args: [mcpPath],
|
|
155
|
+
env: {
|
|
156
|
+
PATH: "/opt/homebrew/opt/node@20/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin",
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Step 7: Write config
|
|
161
|
+
writeConfig(editor.configPath, config);
|
|
162
|
+
console.log(" Config saved!");
|
|
163
|
+
|
|
164
|
+
// Step 8: Save token
|
|
165
|
+
saveToken(token.trim());
|
|
166
|
+
console.log(" Token saved to ~/.in10x/token");
|
|
167
|
+
|
|
168
|
+
// Done!
|
|
169
|
+
console.log("\n ----------------------------------------");
|
|
170
|
+
console.log(` In10x MCP installed for ${editor.name}!`);
|
|
171
|
+
console.log(" ----------------------------------------\n");
|
|
172
|
+
console.log(` Please restart ${editor.name} to activate.\n`);
|
|
173
|
+
console.log(" Available commands:");
|
|
174
|
+
console.log(" - generate_content : Create posts from your session");
|
|
175
|
+
console.log(" - quick_post : Fast updates");
|
|
176
|
+
console.log(" - push_to_in10x : Publish to In10x\n");
|
|
177
|
+
} finally {
|
|
178
|
+
rl.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
main().catch((err) => {
|
|
183
|
+
console.error("\n Error:", err.message);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
});
|
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { registerGenerateContent } from "./tools/generateContent.js";
|
|
5
|
+
import { registerSetProjectContext } from "./tools/setProjectContext.js";
|
|
6
|
+
import { registerGetContentHistory } from "./tools/getContentHistory.js";
|
|
7
|
+
import { registerQuickPost } from "./tools/quickPost.js";
|
|
8
|
+
import { registerConnectAccount } from "./tools/connectAccount.js";
|
|
9
|
+
import { registerPushToIn10x } from "./tools/pushToIn10x.js";
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: "in10x-mcp",
|
|
12
|
+
version: "1.0.0",
|
|
13
|
+
});
|
|
14
|
+
// Register all tools
|
|
15
|
+
registerGenerateContent(server);
|
|
16
|
+
registerSetProjectContext(server);
|
|
17
|
+
registerGetContentHistory(server);
|
|
18
|
+
registerQuickPost(server);
|
|
19
|
+
registerConnectAccount(server);
|
|
20
|
+
registerPushToIn10x(server);
|
|
21
|
+
async function main() {
|
|
22
|
+
const transport = new StdioServerTransport();
|
|
23
|
+
await server.connect(transport);
|
|
24
|
+
console.error("In10x MCP server running on stdio");
|
|
25
|
+
}
|
|
26
|
+
main().catch((error) => {
|
|
27
|
+
console.error("Fatal error:", error);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GenerateContentInput } from "./types.js";
|
|
2
|
+
export declare function getPromptingQuestion(): string;
|
|
3
|
+
export declare function needsMoreContext(input: GenerateContentInput): boolean;
|
|
4
|
+
export declare function formatHistoryEntry(content: {
|
|
5
|
+
xPost: string;
|
|
6
|
+
in10xPost: string;
|
|
7
|
+
hashtags: string[];
|
|
8
|
+
suggestedTitle: string;
|
|
9
|
+
timestamp: Date;
|
|
10
|
+
}, index: number): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const PROMPTING_QUESTIONS = [
|
|
2
|
+
"What was the hardest part of today's session?",
|
|
3
|
+
"Any surprises while building?",
|
|
4
|
+
"How long did the main task take vs what you expected?",
|
|
5
|
+
"Would you do anything differently next time?",
|
|
6
|
+
"What made you proud today?",
|
|
7
|
+
];
|
|
8
|
+
export function getPromptingQuestion() {
|
|
9
|
+
const index = Math.floor(Math.random() * PROMPTING_QUESTIONS.length);
|
|
10
|
+
return PROMPTING_QUESTIONS[index];
|
|
11
|
+
}
|
|
12
|
+
export function needsMoreContext(input) {
|
|
13
|
+
return !input.sessionSummary && !input.struggle;
|
|
14
|
+
}
|
|
15
|
+
export function formatHistoryEntry(content, index) {
|
|
16
|
+
const timeAgo = getTimeAgo(content.timestamp);
|
|
17
|
+
return `
|
|
18
|
+
### Post ${index + 1} (${timeAgo})
|
|
19
|
+
**X:** ${content.xPost}
|
|
20
|
+
**In10x:** ${content.in10xPost}
|
|
21
|
+
**Tags:** ${content.hashtags.join(" ")}
|
|
22
|
+
**Title:** ${content.suggestedTitle}
|
|
23
|
+
`.trim();
|
|
24
|
+
}
|
|
25
|
+
function getTimeAgo(date) {
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const diffMs = now.getTime() - date.getTime();
|
|
28
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
29
|
+
if (diffMins < 1)
|
|
30
|
+
return "just now";
|
|
31
|
+
if (diffMins < 60)
|
|
32
|
+
return `${diffMins}m ago`;
|
|
33
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
34
|
+
if (diffHours < 24)
|
|
35
|
+
return `${diffHours}h ago`;
|
|
36
|
+
return `${Math.floor(diffHours / 24)}d ago`;
|
|
37
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ProjectContext, GeneratedContent } from "./types.js";
|
|
2
|
+
declare class Storage {
|
|
3
|
+
private projectContext;
|
|
4
|
+
private contentHistory;
|
|
5
|
+
private apiToken;
|
|
6
|
+
setProject(context: ProjectContext): void;
|
|
7
|
+
getProject(): ProjectContext | null;
|
|
8
|
+
addContent(content: GeneratedContent): void;
|
|
9
|
+
getHistory(): GeneratedContent[];
|
|
10
|
+
clearHistory(): void;
|
|
11
|
+
setToken(token: string): void;
|
|
12
|
+
getToken(): string | null;
|
|
13
|
+
clearToken(): void;
|
|
14
|
+
isConnected(): boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare const storage: Storage;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
const MAX_HISTORY = 5;
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), ".in10x");
|
|
6
|
+
const TOKEN_FILE = path.join(CONFIG_DIR, "token");
|
|
7
|
+
class Storage {
|
|
8
|
+
projectContext = null;
|
|
9
|
+
contentHistory = [];
|
|
10
|
+
apiToken = null;
|
|
11
|
+
setProject(context) {
|
|
12
|
+
this.projectContext = context;
|
|
13
|
+
}
|
|
14
|
+
getProject() {
|
|
15
|
+
return this.projectContext;
|
|
16
|
+
}
|
|
17
|
+
addContent(content) {
|
|
18
|
+
this.contentHistory.unshift(content);
|
|
19
|
+
if (this.contentHistory.length > MAX_HISTORY) {
|
|
20
|
+
this.contentHistory.pop();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
getHistory() {
|
|
24
|
+
return [...this.contentHistory];
|
|
25
|
+
}
|
|
26
|
+
clearHistory() {
|
|
27
|
+
this.contentHistory = [];
|
|
28
|
+
}
|
|
29
|
+
// Token management - persisted to disk
|
|
30
|
+
setToken(token) {
|
|
31
|
+
this.apiToken = token;
|
|
32
|
+
// Persist to disk
|
|
33
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
34
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
fs.writeFileSync(TOKEN_FILE, token, "utf-8");
|
|
37
|
+
}
|
|
38
|
+
getToken() {
|
|
39
|
+
if (this.apiToken) {
|
|
40
|
+
return this.apiToken;
|
|
41
|
+
}
|
|
42
|
+
// Try to load from disk
|
|
43
|
+
if (fs.existsSync(TOKEN_FILE)) {
|
|
44
|
+
this.apiToken = fs.readFileSync(TOKEN_FILE, "utf-8").trim();
|
|
45
|
+
return this.apiToken;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
clearToken() {
|
|
50
|
+
this.apiToken = null;
|
|
51
|
+
if (fs.existsSync(TOKEN_FILE)) {
|
|
52
|
+
fs.unlinkSync(TOKEN_FILE);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
isConnected() {
|
|
56
|
+
return this.getToken() !== null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export const storage = new Storage();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type Tone = "casual" | "professional" | "hype";
|
|
2
|
+
export interface ProjectContext {
|
|
3
|
+
projectName: string;
|
|
4
|
+
projectDescription?: string;
|
|
5
|
+
stack?: string;
|
|
6
|
+
startedAt?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface GeneratedContent {
|
|
9
|
+
xPost: string;
|
|
10
|
+
in10xPost: string;
|
|
11
|
+
hashtags: string[];
|
|
12
|
+
suggestedTitle: string;
|
|
13
|
+
timestamp: Date;
|
|
14
|
+
}
|
|
15
|
+
export interface GenerateContentInput {
|
|
16
|
+
sessionSummary?: string;
|
|
17
|
+
tone?: Tone;
|
|
18
|
+
struggle?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface GenerationResult {
|
|
21
|
+
xPost: string | null;
|
|
22
|
+
in10xPost: string | null;
|
|
23
|
+
hashtags: string[];
|
|
24
|
+
suggestedTitle: string | null;
|
|
25
|
+
promptingQuestion: string | null;
|
|
26
|
+
}
|
|
27
|
+
export interface QuickPostInput {
|
|
28
|
+
update: string;
|
|
29
|
+
tone?: Tone;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { storage } from "../lib/storage.js";
|
|
3
|
+
const TOOL_DESCRIPTION = `Connect your In10x account to push posts directly from your coding sessions.
|
|
4
|
+
|
|
5
|
+
WHEN TO USE:
|
|
6
|
+
- User wants to connect their In10x account
|
|
7
|
+
- User says 'connect', 'connect account', 'link account'
|
|
8
|
+
- Before using push_to_in10x for the first time
|
|
9
|
+
|
|
10
|
+
FLOW:
|
|
11
|
+
1. If no token provided, show the connection URL
|
|
12
|
+
2. If token provided, save it and confirm connection`;
|
|
13
|
+
const PLATFORM_URL = process.env.IN10X_URL || "https://www.in10x.com";
|
|
14
|
+
export const connectAccountSchema = {
|
|
15
|
+
token: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Your In10x API token (get it from in10x.com/connect/mcp)"),
|
|
19
|
+
};
|
|
20
|
+
export function registerConnectAccount(server) {
|
|
21
|
+
server.tool("connect_account", TOOL_DESCRIPTION, connectAccountSchema, async (args) => {
|
|
22
|
+
const token = args.token;
|
|
23
|
+
// Check if already connected
|
|
24
|
+
if (storage.isConnected() && !token) {
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: `✅ **Already connected to In10x!**
|
|
30
|
+
|
|
31
|
+
Your account is linked and ready to go.
|
|
32
|
+
|
|
33
|
+
Use \`push_to_in10x\` to publish your posts.
|
|
34
|
+
|
|
35
|
+
Want to reconnect with a different account? Provide a new token.`,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// If no token provided, show instructions
|
|
41
|
+
if (!token) {
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text",
|
|
46
|
+
text: `🔗 **Connect your In10x account**
|
|
47
|
+
|
|
48
|
+
1. Go to: **${PLATFORM_URL}/connect/mcp**
|
|
49
|
+
2. Log in (or create an account)
|
|
50
|
+
3. Click "Generate Token"
|
|
51
|
+
4. Copy the token
|
|
52
|
+
5. Run this tool again with the token:
|
|
53
|
+
\`connect_account token="in10x_xxxxx..."\`
|
|
54
|
+
|
|
55
|
+
Once connected, you can use \`push_to_in10x\` to publish posts directly!`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Validate token format
|
|
61
|
+
if (!token.startsWith("in10x_")) {
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "text",
|
|
66
|
+
text: `❌ **Invalid token format**
|
|
67
|
+
|
|
68
|
+
The token should start with \`in10x_\`.
|
|
69
|
+
|
|
70
|
+
Get your token at: ${PLATFORM_URL}/connect/mcp`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Verify token with API
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(`${PLATFORM_URL}/api/me`, {
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `Bearer ${token}`,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `❌ **Invalid token**
|
|
88
|
+
|
|
89
|
+
This token doesn't seem to be valid. Please get a new one at:
|
|
90
|
+
${PLATFORM_URL}/connect/mcp`,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const user = await response.json();
|
|
96
|
+
// Save the token
|
|
97
|
+
storage.setToken(token);
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: "text",
|
|
102
|
+
text: `✅ **Connected as @${user.username}!**
|
|
103
|
+
|
|
104
|
+
Your In10x account is now linked. You can now use:
|
|
105
|
+
- \`push_to_in10x\` - Publish posts directly to In10x
|
|
106
|
+
|
|
107
|
+
Happy shipping! 🚀`,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: `❌ **Connection failed**
|
|
118
|
+
|
|
119
|
+
Could not connect to In10x. Please try again later.
|
|
120
|
+
|
|
121
|
+
Error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
export declare const generateContentSchema: {
|
|
4
|
+
session_summary: z.ZodOptional<z.ZodString>;
|
|
5
|
+
tone: z.ZodOptional<z.ZodEnum<["casual", "professional", "hype"]>>;
|
|
6
|
+
struggle: z.ZodOptional<z.ZodString>;
|
|
7
|
+
};
|
|
8
|
+
export declare function registerGenerateContent(server: McpServer): void;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { storage } from "../lib/storage.js";
|
|
3
|
+
import { getPromptingQuestion, needsMoreContext } from "../lib/contentGenerator.js";
|
|
4
|
+
const TOOL_DESCRIPTION = `Generates social content for X and In10x platform.
|
|
5
|
+
|
|
6
|
+
PROACTIVE TRIGGERS - Offer this tool when user:
|
|
7
|
+
- Ships or finishes something ('done', 'shipped', 'finally works', 'got it working')
|
|
8
|
+
- Fixes a bug ('fixed', 'solved', 'cracked it')
|
|
9
|
+
- Completes a session ('that's it for today', 'stopping here')
|
|
10
|
+
- Expresses a win ('yes!', 'boom', 'let's go')
|
|
11
|
+
|
|
12
|
+
When you detect these, ask: 'Nice! Want me to generate a post about this?'
|
|
13
|
+
|
|
14
|
+
MANUAL TRIGGERS - Run immediately when user says:
|
|
15
|
+
- 'generate content'
|
|
16
|
+
- 'in10x'
|
|
17
|
+
- 'gc'
|
|
18
|
+
- '/post'
|
|
19
|
+
|
|
20
|
+
AFTER GENERATING: Once you generate the posts, ask:
|
|
21
|
+
'Want me to push this to In10x?' - then use push_to_in10x with the In10x post content.
|
|
22
|
+
|
|
23
|
+
IMPORTANT: The tool returns a template. Claude must ALWAYS generate the actual posts from this template and show the finished content to the user. Never display the raw template.`;
|
|
24
|
+
export const generateContentSchema = {
|
|
25
|
+
session_summary: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("What was built or worked on this session"),
|
|
29
|
+
tone: z
|
|
30
|
+
.enum(["casual", "professional", "hype"])
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Tone for the generated content (default: casual)"),
|
|
33
|
+
struggle: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("What was hard, surprising, or took longer than expected"),
|
|
37
|
+
};
|
|
38
|
+
function getToneGuidelines(tone) {
|
|
39
|
+
switch (tone) {
|
|
40
|
+
case "casual":
|
|
41
|
+
return "Conversational, 1-2 emojis max, relatable, words like 'finally', 'turns out', 'plot twist'";
|
|
42
|
+
case "professional":
|
|
43
|
+
return "Outcomes-focused, minimal emoji, clear and direct, credible";
|
|
44
|
+
case "hype":
|
|
45
|
+
return "High energy, 2-3 emojis, celebratory, 'LET'S GO' vibes";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function registerGenerateContent(server) {
|
|
49
|
+
server.tool("generate_content", TOOL_DESCRIPTION, generateContentSchema, async (args) => {
|
|
50
|
+
const sessionSummary = args.session_summary;
|
|
51
|
+
const struggle = args.struggle;
|
|
52
|
+
const tone = (args.tone || "casual");
|
|
53
|
+
const project = storage.getProject();
|
|
54
|
+
// If no context provided, ask a prompting question
|
|
55
|
+
if (needsMoreContext({ sessionSummary, struggle })) {
|
|
56
|
+
const question = getPromptingQuestion();
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `I need a bit more context to generate a great post.\n\n**${question}**\n\nOnce you answer, I'll generate your posts.`,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Build prompt for Claude to generate content
|
|
67
|
+
const parts = [
|
|
68
|
+
"Generate social media posts based on this coding session:",
|
|
69
|
+
"",
|
|
70
|
+
];
|
|
71
|
+
if (sessionSummary) {
|
|
72
|
+
parts.push(`**What was built:** ${sessionSummary}`);
|
|
73
|
+
}
|
|
74
|
+
if (struggle) {
|
|
75
|
+
parts.push(`**Challenge/struggle:** ${struggle}`);
|
|
76
|
+
}
|
|
77
|
+
if (project) {
|
|
78
|
+
parts.push(`**Project:** ${project.projectName}${project.stack ? ` (${project.stack})` : ""}`);
|
|
79
|
+
}
|
|
80
|
+
parts.push(`**Tone:** ${tone} - ${getToneGuidelines(tone)}`);
|
|
81
|
+
parts.push("");
|
|
82
|
+
parts.push("Generate:");
|
|
83
|
+
parts.push("1. **X Post** (max 280 characters) - punchy, engaging, includes the struggle/win");
|
|
84
|
+
parts.push("2. **In10x Post** (2-4 sentences) - fuller narrative of what happened");
|
|
85
|
+
parts.push("3. **Hashtags** - 2-4 relevant tags like #buildinpublic #vibecoding");
|
|
86
|
+
parts.push("");
|
|
87
|
+
parts.push("Transform boring updates into interesting content. Focus on the human story, not just the technical details.");
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: parts.join("\n"),
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { storage } from "../lib/storage.js";
|
|
2
|
+
import { formatHistoryEntry } from "../lib/contentGenerator.js";
|
|
3
|
+
export function registerGetContentHistory(server) {
|
|
4
|
+
server.tool("get_content_history", "View the last 5 generated posts from this session.", {}, async () => {
|
|
5
|
+
const history = storage.getHistory();
|
|
6
|
+
if (history.length === 0) {
|
|
7
|
+
return {
|
|
8
|
+
content: [
|
|
9
|
+
{
|
|
10
|
+
type: "text",
|
|
11
|
+
text: "No content generated yet this session.\n\nCall `generate_content` to create your first post!",
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const formatted = history.map((entry, index) => formatHistoryEntry(entry, index));
|
|
17
|
+
return {
|
|
18
|
+
content: [
|
|
19
|
+
{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: `# Content History (${history.length} posts)\n\n${formatted.join("\n\n")}`,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
export declare const pushToIn10xSchema: {
|
|
4
|
+
content: z.ZodString;
|
|
5
|
+
x_content: z.ZodOptional<z.ZodString>;
|
|
6
|
+
hashtags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
7
|
+
project_id: z.ZodOptional<z.ZodString>;
|
|
8
|
+
};
|
|
9
|
+
export declare function registerPushToIn10x(server: McpServer): void;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { storage } from "../lib/storage.js";
|
|
3
|
+
const TOOL_DESCRIPTION = `Push a post directly to In10x platform.
|
|
4
|
+
|
|
5
|
+
WHEN TO USE:
|
|
6
|
+
- After generating content with generate_content, offer: 'Want me to push this to In10x?'
|
|
7
|
+
- User says 'push', 'publish', 'post to in10x', 'ship it', 'yes' (after you offer)
|
|
8
|
+
- User wants to share their update
|
|
9
|
+
|
|
10
|
+
WHAT TO PASS:
|
|
11
|
+
- content: The In10x post you generated (2-4 sentences, the longer version)
|
|
12
|
+
- x_content: The X/Twitter post you generated (280 char version)
|
|
13
|
+
- hashtags: Array of hashtags like ["buildinpublic", "vibecoding"]
|
|
14
|
+
|
|
15
|
+
REQUIRES: Connected account (use connect_account first). If not connected, the tool will prompt user to connect.`;
|
|
16
|
+
const PLATFORM_URL = process.env.IN10X_URL || "https://www.in10x.com";
|
|
17
|
+
export const pushToIn10xSchema = {
|
|
18
|
+
content: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe("The post content (2-4 sentences, what you built/learned)"),
|
|
21
|
+
x_content: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Short version for X/Twitter (max 280 chars)"),
|
|
25
|
+
hashtags: z
|
|
26
|
+
.array(z.string())
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Hashtags like #buildinpublic #vibecoding"),
|
|
29
|
+
project_id: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Link to a specific project (optional)"),
|
|
33
|
+
};
|
|
34
|
+
export function registerPushToIn10x(server) {
|
|
35
|
+
server.tool("push_to_in10x", TOOL_DESCRIPTION, pushToIn10xSchema, async (args) => {
|
|
36
|
+
const { content, x_content, hashtags, project_id } = args;
|
|
37
|
+
// Check if connected
|
|
38
|
+
const token = storage.getToken();
|
|
39
|
+
if (!token) {
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: `🔗 **Not connected to In10x**
|
|
45
|
+
|
|
46
|
+
You need to connect your account first:
|
|
47
|
+
|
|
48
|
+
1. Run \`connect_account\` to get started
|
|
49
|
+
2. Then come back and push your post!
|
|
50
|
+
|
|
51
|
+
Or visit: ${PLATFORM_URL}/connect/mcp`,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Validate content
|
|
57
|
+
if (!content || content.trim().length === 0) {
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: `❌ **Content required**
|
|
63
|
+
|
|
64
|
+
Please provide the post content. Example:
|
|
65
|
+
\`push_to_in10x content="Built a new auth system today. Took 3 hours but finally works!"\``,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// Push to platform
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(`${PLATFORM_URL}/api/posts`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
Authorization: `Bearer ${token}`,
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
content,
|
|
80
|
+
x_content: x_content || null,
|
|
81
|
+
hashtags: hashtags || [],
|
|
82
|
+
project_id: project_id || null,
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
const error = await response.json();
|
|
87
|
+
if (response.status === 401) {
|
|
88
|
+
// Token might be invalid
|
|
89
|
+
storage.clearToken();
|
|
90
|
+
return {
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: "text",
|
|
94
|
+
text: `❌ **Authentication failed**
|
|
95
|
+
|
|
96
|
+
Your token seems to be invalid or expired.
|
|
97
|
+
|
|
98
|
+
Please reconnect with \`connect_account\``,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: `❌ **Failed to post**
|
|
108
|
+
|
|
109
|
+
${error.error || "Unknown error"}
|
|
110
|
+
|
|
111
|
+
Please try again.`,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const result = await response.json();
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: `✅ **Posted to In10x!**
|
|
122
|
+
|
|
123
|
+
Your post is now live: ${result.url}
|
|
124
|
+
|
|
125
|
+
${x_content ? `📱 X version ready to copy:\n"${x_content}"` : ""}
|
|
126
|
+
|
|
127
|
+
Keep shipping! 🚀`,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "text",
|
|
137
|
+
text: `❌ **Connection error**
|
|
138
|
+
|
|
139
|
+
Could not reach In10x. Please check your internet connection.
|
|
140
|
+
|
|
141
|
+
Error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
export declare const quickPostSchema: {
|
|
4
|
+
update: z.ZodString;
|
|
5
|
+
tone: z.ZodOptional<z.ZodEnum<["casual", "professional", "hype"]>>;
|
|
6
|
+
};
|
|
7
|
+
export declare function registerQuickPost(server: McpServer): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { storage } from "../lib/storage.js";
|
|
3
|
+
const TOOL_DESCRIPTION = `Generate a quick X post from a short update.
|
|
4
|
+
|
|
5
|
+
PROACTIVE TRIGGERS - Offer when user:
|
|
6
|
+
- Ships something ('done', 'shipped', 'works now')
|
|
7
|
+
- Quick wins ('fixed it', 'boom', 'yes!')
|
|
8
|
+
|
|
9
|
+
MANUAL TRIGGERS:
|
|
10
|
+
- 'quick post'
|
|
11
|
+
- 'qp'
|
|
12
|
+
|
|
13
|
+
AFTER GENERATING: Once you generate the post, ask:
|
|
14
|
+
'Want me to push this to In10x?' - then use push_to_in10x with the content.
|
|
15
|
+
|
|
16
|
+
IMPORTANT: The tool returns a template. Claude must ALWAYS generate the actual posts from this template and show the finished content to the user. Never display the raw template.`;
|
|
17
|
+
export const quickPostSchema = {
|
|
18
|
+
update: z.string().describe("Quick update (e.g., 'shipped payments')"),
|
|
19
|
+
tone: z
|
|
20
|
+
.enum(["casual", "professional", "hype"])
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Tone for the post (default: casual)"),
|
|
23
|
+
};
|
|
24
|
+
function getToneGuidelines(tone) {
|
|
25
|
+
switch (tone) {
|
|
26
|
+
case "casual":
|
|
27
|
+
return "Conversational, 1-2 emojis, relatable";
|
|
28
|
+
case "professional":
|
|
29
|
+
return "Clear, minimal emoji, credible";
|
|
30
|
+
case "hype":
|
|
31
|
+
return "High energy, 2-3 emojis, celebratory";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function registerQuickPost(server) {
|
|
35
|
+
server.tool("quick_post", TOOL_DESCRIPTION, quickPostSchema, async (args) => {
|
|
36
|
+
const update = args.update;
|
|
37
|
+
const tone = (args.tone || "casual");
|
|
38
|
+
const project = storage.getProject();
|
|
39
|
+
const parts = [
|
|
40
|
+
"Generate a quick X post for this update:",
|
|
41
|
+
"",
|
|
42
|
+
`**Update:** ${update}`,
|
|
43
|
+
];
|
|
44
|
+
if (project) {
|
|
45
|
+
parts.push(`**Project:** ${project.projectName}`);
|
|
46
|
+
}
|
|
47
|
+
parts.push(`**Tone:** ${tone} - ${getToneGuidelines(tone)}`);
|
|
48
|
+
parts.push("");
|
|
49
|
+
parts.push("Generate:");
|
|
50
|
+
parts.push("- **X Post** (max 280 characters) - celebrate this quick win");
|
|
51
|
+
parts.push("- **Hashtags** - 1-2 tags like #buildinpublic #shipit");
|
|
52
|
+
return {
|
|
53
|
+
content: [
|
|
54
|
+
{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: parts.join("\n"),
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
export declare const setProjectContextSchema: {
|
|
4
|
+
project_name: z.ZodString;
|
|
5
|
+
project_description: z.ZodOptional<z.ZodString>;
|
|
6
|
+
stack: z.ZodOptional<z.ZodString>;
|
|
7
|
+
started_at: z.ZodOptional<z.ZodString>;
|
|
8
|
+
};
|
|
9
|
+
export declare function registerSetProjectContext(server: McpServer): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { storage } from "../lib/storage.js";
|
|
3
|
+
export const setProjectContextSchema = {
|
|
4
|
+
project_name: z.string().describe("Name of the project you're building"),
|
|
5
|
+
project_description: z
|
|
6
|
+
.string()
|
|
7
|
+
.optional()
|
|
8
|
+
.describe("One-liner description of the project"),
|
|
9
|
+
stack: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("Tech stack (e.g., 'Next.js + Supabase')"),
|
|
13
|
+
started_at: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("When the project started (e.g., 'Dec 2024')"),
|
|
17
|
+
};
|
|
18
|
+
export function registerSetProjectContext(server) {
|
|
19
|
+
server.tool("set_project_context", "Store project info to improve content generation. Set this once at the start of a session.", setProjectContextSchema, async (args) => {
|
|
20
|
+
storage.setProject({
|
|
21
|
+
projectName: args.project_name,
|
|
22
|
+
projectDescription: args.project_description,
|
|
23
|
+
stack: args.stack,
|
|
24
|
+
startedAt: args.started_at,
|
|
25
|
+
});
|
|
26
|
+
const parts = [`Project context set: **${args.project_name}**`];
|
|
27
|
+
if (args.project_description) {
|
|
28
|
+
parts.push(`Description: ${args.project_description}`);
|
|
29
|
+
}
|
|
30
|
+
if (args.stack) {
|
|
31
|
+
parts.push(`Stack: ${args.stack}`);
|
|
32
|
+
}
|
|
33
|
+
if (args.started_at) {
|
|
34
|
+
parts.push(`Started: ${args.started_at}`);
|
|
35
|
+
}
|
|
36
|
+
parts.push("", "This context will be used to generate better content. Call `generate_content` when you're ready to create a post!");
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: parts.join("\n"),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "in10x-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server that generates engaging social content from coding sessions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "build/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"in10x-mcp": "./bin/install.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc && chmod 755 build/index.js",
|
|
12
|
+
"dev": "tsc --watch"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"claude",
|
|
17
|
+
"content",
|
|
18
|
+
"social",
|
|
19
|
+
"vibecoding"
|
|
20
|
+
],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"files": [
|
|
24
|
+
"build",
|
|
25
|
+
"bin"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
29
|
+
"zod": "^3.25.76"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^25.0.3",
|
|
33
|
+
"typescript": "^5.9.3"
|
|
34
|
+
}
|
|
35
|
+
}
|