import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Hono } from "hono";
import { layout, homeContent } from "./utils";
import * as xApi from "./x-api";
type Bindings = Env;
const app = new Hono<{
Bindings: Bindings;
}>();
type Props = {
bearerToken: string;
};
type State = null;
export class MyMCP extends McpAgent<Bindings, State, Props> {
server = new McpServer({
name: "Demo",
version: "1.0.0",
});
async init() {
// Get user profile tool
this.server.tool(
"getUserProfile",
"Get a user's profile information on twitter or x.com",
{
username: z.string(),
},
async ({ username }) => {
try {
const user = await xApi.getUserProfile(
this.env,
username
);
console.log("🚀 | user:", user)
return {
content: [
{
type: "text",
text: `User Profile for @${username}:
Name: ${user.name}
Description: ${user.description}
Followers: ${user.public_metrics?.followers_count}
Following: ${user.public_metrics?.following_count}
Tweets: ${user.public_metrics?.tweet_count}`,
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error fetching user profile: ${error.message}`,
},
],
};
}
}
);
// Post tweet tool
this.server.tool(
"postTweet",
"Post a new tweet on x.com",
{
text: z.string().max(280),
},
async ({ text }) => {
try {
const tweet = await xApi.postTweet(this.env,text);
return {
content: [
{
type: "text",
text: `Tweet posted successfully! Tweet ID: ${tweet.id}`,
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error posting tweet: ${error.message}`,
},
],
};
}
}
);
// Search tweets tool
this.server.tool(
"searchTweets",
"Search for recent tweets on x.com",
{
query: z.string(),
count: z.number().min(10).max(100).default(10),
},
async ({ query, count }) => {
try {
const { tweets, users } = await xApi.searchTweets(
this.env,
query,
count
);
// const formattedTweets = tweets.map((tweet, index) => {
// const author = users.find((u) => u.id === tweet.authorId);
// return {
// position: index + 1,
// author: {
// username: author?.username || "unknown",
// },
// content: tweet.text,
// metrics: tweet.metrics,
// url: `https://twitter.com/${author?.username}/status/${tweet.id}`,
// };
// });
// return {
// content: [
// {
// type: "text",
// text: `Found ${
// tweets.length
// } tweets for query "${query}":\n\n${formattedTweets
// .map(
// (t) =>
// `${t.position}. @${t.author.username}: ${t.content}\n` +
// ` Likes: ${t.metrics.likes}, RTs: ${t.metrics.retweets}, Replies: ${t.metrics.replies}\n` +
// ` URL: ${t.url}\n`
// )
// .join("\n")}`,
// },
// ],
// };
return { content: [tweets, users] };
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error searching tweets: ${error.message}`,
},
],
};
}
}
);
}
}
// Render a basic homepage placeholder to make sure the app is up
app.get("/", async (c) => {
const content = await homeContent(c.req.raw);
return c.html(layout(content, "X.com MCP Server"));
});
app.mount("/", (req, env, ctx) => {
// This could technically be pulled out into a middleware function, but is left here for clarity
const authHeader = req.headers.get("authorization");
if (!authHeader) {
return new Response("Unauthorized", { status: 401 });
}
ctx.props = {
bearerToken: authHeader,
// could also add arbitrary headers/parameters here to pass into the MCP client
};
return MyMCP.mount("/sse").fetch(req, env, ctx);
});
export default app;