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 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
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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,6 @@
1
+ import { z } from "zod";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ export declare const connectAccountSchema: {
4
+ token: z.ZodOptional<z.ZodString>;
5
+ };
6
+ export declare function registerConnectAccount(server: McpServer): void;
@@ -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,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerGetContentHistory(server: McpServer): void;
@@ -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
+ }