← back to Linkedin Agent
src/mcp/server.ts
133 lines
/**
* MCP server (stdio) — exposes LinkedIn Agent tools to Claude Code.
*
* Tools:
* linkedin_post — create a text post on Steve's personal feed
* linkedin_post_page — create a text post on a Company Page
* linkedin_comment — comment on a given post URN
* linkedin_react — react to a post URN (LIKE/PRAISE/etc.)
* linkedin_compose_preview — preview composed post (with bio tagline)
*
* Wire into Claude Code via:
* ~/.claude.json → mcpServers → linkedin-agent → command:
* "node --import tsx /Users/macstudio3/Projects/linkedin-agent/src/mcp/server.ts"
*
* 2026-05-06 (tick 53): scaffold landed.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { postText, postOnPage, commentOnPost, react } from '../lib/linkedin.js';
import { composePost } from '../lib/compose.js';
const server = new Server(
{ name: 'linkedin-agent', version: '0.1.0' },
{ capabilities: { tools: {} } },
);
const TOOLS = [
{
name: 'linkedin_post',
description: 'Create a text post on Steve\'s personal LinkedIn feed. Composes with STEVE_BIO tagline by default.',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: 'Post body (max ~3000 chars). Plain text + LinkedIn-supported markdown.' },
includeBio: { type: 'boolean', description: 'Append the rotating "Building lately:" tagline. Default true.' },
},
required: ['text'],
},
},
{
name: 'linkedin_post_page',
description: 'Create a text post on Steve\'s Company Page (LINKEDIN_ORG_URN env). NO tagline — page posts are first-person from the brand.',
inputSchema: {
type: 'object',
properties: { text: { type: 'string' } },
required: ['text'],
},
},
{
name: 'linkedin_comment',
description: 'Comment on a given LinkedIn post URN.',
inputSchema: {
type: 'object',
properties: {
postUrn: { type: 'string', description: 'Full URN like urn:li:share:1234567890' },
text: { type: 'string' },
},
required: ['postUrn', 'text'],
},
},
{
name: 'linkedin_react',
description: 'React to a post URN. Default reaction is LIKE.',
inputSchema: {
type: 'object',
properties: {
postUrn: { type: 'string' },
reaction: { type: 'string', enum: ['LIKE', 'PRAISE', 'EMPATHY', 'INTEREST', 'APPRECIATION', 'MAYBE'] },
},
required: ['postUrn'],
},
},
{
name: 'linkedin_compose_preview',
description: 'Preview the final composed post (draft + tagline) without sending.',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string' },
includeBio: { type: 'boolean' },
},
required: ['text'],
},
},
];
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const a = (args || {}) as Record<string, unknown>;
try {
if (name === 'linkedin_post') {
const final = composePost(String(a.text), { tagline: a.includeBio !== false });
const result = await postText(final);
return { content: [{ type: 'text', text: `Posted. URN: ${result.id}\n\n---\n${final}` }] };
}
if (name === 'linkedin_post_page') {
const final = composePost(String(a.text), { tagline: false });
const result = await postOnPage(final);
return { content: [{ type: 'text', text: `Posted on Company Page. URN: ${result.id}` }] };
}
if (name === 'linkedin_comment') {
const result = await commentOnPost(String(a.postUrn), String(a.text));
return { content: [{ type: 'text', text: `Comment posted. URN: ${result.id}` }] };
}
if (name === 'linkedin_react') {
await react(String(a.postUrn), (a.reaction as 'LIKE') || 'LIKE');
return { content: [{ type: 'text', text: `Reacted ${a.reaction || 'LIKE'} on ${a.postUrn}.` }] };
}
if (name === 'linkedin_compose_preview') {
const final = composePost(String(a.text), { tagline: a.includeBio !== false });
return { content: [{ type: 'text', text: final }] };
}
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
} catch (err) {
return {
content: [{ type: 'text', text: `Error: ${(err as Error).message}` }],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('[linkedin-agent MCP] ready (stdio)');