218 lines
8.7 KiB
JavaScript
218 lines
8.7 KiB
JavaScript
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 { JMAPClient } from "./jmap_client.js";
|
|
import * as dotenv from "dotenv";
|
|
dotenv.config();
|
|
const server = new Server({
|
|
name: "stalwart_mcp",
|
|
version: "1.0.0",
|
|
}, {
|
|
capabilities: {
|
|
tools: {},
|
|
},
|
|
});
|
|
const JMAP_URL = process.env.STALWART_JMAP_URL || "https://mail.infra.mintel.me";
|
|
const JMAP_USER = process.env.STALWART_USER || "antigravity";
|
|
const JMAP_PASSWORD = process.env.STALWART_APP_PASSWORD || "90e21uh0eondwqa9i0owsdn";
|
|
const client = new JMAPClient(JMAP_URL, JMAP_USER, JMAP_PASSWORD);
|
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
return {
|
|
tools: [
|
|
{
|
|
name: "stalwart_list_mailboxes",
|
|
description: "List all mailboxes (folders) for the authenticated account.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {},
|
|
},
|
|
},
|
|
{
|
|
name: "stalwart_search_emails",
|
|
description: "Search for emails in a specific mailbox or across all mailboxes.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
mailboxId: {
|
|
type: "string",
|
|
description: "Optional mailbox ID to filter the search.",
|
|
},
|
|
query: {
|
|
type: "string",
|
|
description: "Search query (e.g., 'from:someone', 'subject:hello').",
|
|
},
|
|
limit: {
|
|
type: "number",
|
|
description: "Maximum number of emails to return (default 10).",
|
|
}
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "stalwart_read_email",
|
|
description: "Read the full contents of a specific email by its ID.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
emailId: {
|
|
type: "string",
|
|
description: "The ID of the email to read.",
|
|
},
|
|
},
|
|
required: ["emailId"],
|
|
},
|
|
},
|
|
{
|
|
name: "stalwart_send_email",
|
|
description: "Send a new email.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
to: {
|
|
type: "string",
|
|
description: "Recipient email address.",
|
|
},
|
|
subject: {
|
|
type: "string",
|
|
description: "Subject of the email.",
|
|
},
|
|
textBody: {
|
|
type: "string",
|
|
description: "Plain text body of the email.",
|
|
},
|
|
htmlBody: {
|
|
type: "string",
|
|
description: "Optional HTML body of the email.",
|
|
}
|
|
},
|
|
required: ["to", "subject", "textBody"],
|
|
},
|
|
}
|
|
],
|
|
};
|
|
});
|
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
const { name, arguments: args } = request.params;
|
|
try {
|
|
if (name === "stalwart_list_mailboxes") {
|
|
const response = await client.request([
|
|
["Mailbox/get", { accountId: client.getAccountId(), ids: null }, "0"]
|
|
]);
|
|
const mailboxes = response.methodResponses[0][1].list;
|
|
return {
|
|
content: [{ type: "text", text: JSON.stringify(mailboxes, null, 2) }],
|
|
};
|
|
}
|
|
else if (name === "stalwart_search_emails") {
|
|
const { mailboxId, query, limit = 10 } = args;
|
|
// Note: A full search implementation in JMAP is slightly more complex,
|
|
// but we can query Email/query.
|
|
const filter = {};
|
|
if (mailboxId)
|
|
filter.inMailbox = mailboxId;
|
|
if (query)
|
|
filter.text = query; // Simple text search
|
|
const queryResponse = await client.request([
|
|
["Email/query", {
|
|
accountId: client.getAccountId(),
|
|
filter: Object.keys(filter).length > 0 ? filter : null,
|
|
limit
|
|
}, "0"],
|
|
["Email/get", {
|
|
accountId: client.getAccountId(),
|
|
"#ids": {
|
|
resultOf: "0",
|
|
name: "Email/query",
|
|
path: "/ids"
|
|
},
|
|
properties: ["id", "subject", "from", "to", "receivedAt", "preview"]
|
|
}, "1"]
|
|
]);
|
|
const emails = queryResponse.methodResponses[1][1].list;
|
|
return {
|
|
content: [{ type: "text", text: JSON.stringify(emails, null, 2) }],
|
|
};
|
|
}
|
|
else if (name === "stalwart_read_email") {
|
|
const { emailId } = args;
|
|
const response = await client.request([
|
|
["Email/get", {
|
|
accountId: client.getAccountId(),
|
|
ids: [emailId],
|
|
properties: ["id", "subject", "from", "to", "receivedAt", "bodyValues", "textBody", "htmlBody"]
|
|
}, "0"]
|
|
]);
|
|
const email = response.methodResponses[0][1].list[0];
|
|
if (!email) {
|
|
return { content: [{ type: "text", text: `Email with ID ${emailId} not found.` }] };
|
|
}
|
|
return {
|
|
content: [{ type: "text", text: JSON.stringify(email, null, 2) }],
|
|
};
|
|
}
|
|
else if (name === "stalwart_send_email") {
|
|
const { to, subject, textBody, htmlBody } = args;
|
|
const accountId = client.getAccountId();
|
|
const creationId = "draft-" + Date.now();
|
|
const bodyParts = [
|
|
{ partId: "part_text", type: "text/plain" }
|
|
];
|
|
const bodyValues = {
|
|
"part_text": { value: textBody, isEncodingProblem: false, isTruncated: false }
|
|
};
|
|
if (htmlBody) {
|
|
bodyParts.push({ partId: "part_html", type: "text/html" });
|
|
bodyValues["part_html"] = { value: htmlBody, isEncodingProblem: false, isTruncated: false };
|
|
}
|
|
const response = await client.request([
|
|
["Email/set", {
|
|
accountId,
|
|
create: {
|
|
[creationId]: {
|
|
from: [{ email: JMAP_USER.includes("@") ? JMAP_USER : `${JMAP_USER}@mintel.me` }],
|
|
to: [{ email: to }],
|
|
subject: subject,
|
|
bodyValues: bodyValues,
|
|
textBody: [{ partId: "part_text" }],
|
|
...(htmlBody ? { htmlBody: [{ partId: "part_html" }] } : {})
|
|
}
|
|
}
|
|
}, "0"],
|
|
["EmailSubmission/set", {
|
|
accountId,
|
|
create: {
|
|
"send-1": {
|
|
emailId: `#${creationId}`
|
|
}
|
|
}
|
|
}, "1"]
|
|
]);
|
|
return {
|
|
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
|
|
};
|
|
}
|
|
throw new Error(`Unknown tool: ${name}`);
|
|
}
|
|
catch (error) {
|
|
return {
|
|
isError: true,
|
|
content: [{ type: "text", text: `Error executing tool: ${error.message}\n${JSON.stringify(error.response?.data || {})}` }],
|
|
};
|
|
}
|
|
});
|
|
async function run() {
|
|
// Initial connection test
|
|
try {
|
|
await client.connect();
|
|
console.error("Successfully connected to Stalwart JMAP!");
|
|
}
|
|
catch (e) {
|
|
console.error("Failed to connect to Stalwart on startup. Ensure credentials are correct.");
|
|
process.exit(1);
|
|
}
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
console.error("Stalwart MCP Server running on stdio");
|
|
}
|
|
run().catch(console.error);
|
|
//# sourceMappingURL=index.js.map
|