#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import Anthropic from "@anthropic-ai/sdk";
const GENERATE_PROPOSAL_TOOL: Tool = {
name: "generate_proposal",
description:
"Generate a professional freelance project proposal using AI. Creates a complete proposal with executive summary, scope of work, timeline, pricing, and terms.",
inputSchema: {
type: "object" as const,
properties: {
project_description: {
type: "string",
description:
"Detailed description of the project requirements and goals. Should be at least 10 characters.",
},
client_name: {
type: "string",
description: "Name of the client or company the proposal is for.",
},
budget: {
type: "string",
description:
"Budget information - can be hourly rate, fixed price, or budget range (e.g., '$100/hr', '$5000 fixed', '$3000-5000').",
},
timeline: {
type: "string",
description:
"Desired project timeline or deadline (e.g., '2 weeks', 'by March 15', 'ASAP').",
},
tone: {
type: "string",
enum: ["professional", "confident", "friendly"],
description:
"Writing tone for the proposal. Defaults to professional.",
},
services: {
type: "string",
description:
"Optional: Freelancer's services or skills to highlight in the proposal.",
},
},
required: ["project_description", "client_name", "budget", "timeline"],
},
};
async function generateProposal(args: {
project_description: string;
client_name: string;
budget: string;
timeline: string;
tone?: string;
services?: string;
}): Promise<string> {
const { project_description, client_name, budget, timeline, tone, services } =
args;
if (
!project_description ||
typeof project_description !== "string" ||
project_description.trim().length < 10
) {
throw new Error(
"Please provide a project description (at least 10 characters)."
);
}
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
throw new Error(
"ANTHROPIC_API_KEY environment variable is required but not set."
);
}
const client = new Anthropic({ apiKey });
const toneInstructions: Record<string, string> = {
professional:
"Use a professional, polished tone. Be clear, structured, and authoritative.",
confident:
"Use a confident, assertive tone. Show expertise and conviction. Be bold but not arrogant.",
friendly:
"Use a warm, friendly tone. Be approachable and personable while maintaining professionalism.",
};
const toneGuide = toneInstructions[tone || "professional"] || toneInstructions.professional;
const systemPrompt = `You are ProposalPilot, an expert freelance proposal writer. You create compelling, professional project proposals that win deals.
Your proposals are structured, persuasive, and tailored to each project. You write in a way that builds trust and demonstrates expertise.
${toneGuide}
IMPORTANT RULES:
- Write the proposal as plain text with clear section headers (use === for main sections)
- Do NOT use markdown formatting like **, ##, or bullet point markers
- Use clean, readable formatting
- Be specific and detailed but concise
- The proposal is for client: ${client_name}
- Desired timeline: ${timeline}
- Budget/Rate: ${budget}
- Include realistic timelines based on the project scope
- Calculate project pricing based on the provided budget information
- Always include payment terms
- Make the client feel confident in hiring this freelancer`;
const userMessage = `Write a complete project proposal for ${client_name} based on the following:
PROJECT BRIEF:
${project_description}
CLIENT: ${client_name}
BUDGET: ${budget}
TIMELINE: ${timeline}
${services ? `FREELANCER'S SERVICES/SKILLS:\n${services}\n` : ""}
Generate a complete proposal with these sections:
1. EXECUTIVE SUMMARY
A compelling overview that shows understanding of the project and why the freelancer is the right fit. Address ${client_name} directly.
2. SCOPE OF WORK
Detailed breakdown of what will be delivered, organized into clear phases or deliverables.
3. TIMELINE
Realistic project timeline with milestones, working within the ${timeline} timeframe.
4. PRICING
Clear pricing breakdown based on the ${budget} budget. Include a total project cost.
5. TERMS & CONDITIONS
Standard freelance terms including payment schedule, revision policy, and communication expectations.
Write the proposal now. Make it specific to the project described — not generic. Address ${client_name} throughout.`;
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4000,
messages: [
{
role: "user",
content: userMessage,
},
],
system: systemPrompt,
});
const proposalText =
response.content[0].type === "text" ? response.content[0].text : "";
return proposalText;
}
async function main() {
const server = new Server(
{
name: "proposalpilot-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [GENERATE_PROPOSAL_TOOL],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "generate_proposal") {
try {
const proposal = await generateProposal(
args as {
project_description: string;
client_name: string;
budget: string;
timeline: string;
tone?: string;
services?: string;
}
);
return {
content: [
{
type: "text" as const,
text: proposal,
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text" as const,
text: `Error generating proposal: ${message}`,
},
],
isError: true,
};
}
}
return {
content: [
{
type: "text" as const,
text: `Unknown tool: ${name}`,
},
],
isError: true,
};
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("ProposalPilot MCP server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});