Initial commit of Stalwart MCP Server

This commit is contained in:
2026-06-06 08:14:23 +02:00
commit e667786d02
4014 changed files with 927294 additions and 0 deletions

2
build/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=index.d.ts.map

1
build/index.d.ts.map Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":""}

218
build/index.js Normal file
View File

@@ -0,0 +1,218 @@
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

1
build/index.js.map Normal file

File diff suppressed because one or more lines are too long

13
build/jmap_client.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
export declare class JMAPClient {
private baseUrl;
private username;
private appPassword;
private apiUrl;
private accountId;
private client;
constructor(baseUrl: string, username: string, appPassword: string);
connect(): Promise<void>;
request(methodCalls: any[][]): Promise<any>;
getAccountId(): string;
}
//# sourceMappingURL=jmap_client.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"jmap_client.d.ts","sourceRoot":"","sources":["../jmap_client.ts"],"names":[],"mappings":"AAEA,qBAAa,UAAU;IAMf,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,WAAW;IAPvB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,MAAM,CAAgB;gBAGlB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM;IAYzB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA6BxB,OAAO,CAAC,WAAW,EAAE,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC;IAiBjD,YAAY,IAAI,MAAM;CAGzB"}

66
build/jmap_client.js Normal file
View File

@@ -0,0 +1,66 @@
import axios, {} from 'axios';
export class JMAPClient {
baseUrl;
username;
appPassword;
apiUrl = '';
accountId = '';
client;
constructor(baseUrl, username, appPassword) {
this.baseUrl = baseUrl;
this.username = username;
this.appPassword = appPassword;
const auth = Buffer.from(`${this.username}:${this.appPassword}`).toString('base64');
this.client = axios.create({
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
}
});
}
async connect() {
// Fetch session
// Stalwart redirects /.well-known/jmap to /jmap/session
const sessionUrl = this.baseUrl.endsWith('/') ? `${this.baseUrl}jmap/session` : `${this.baseUrl}/jmap/session`;
try {
const response = await this.client.get(sessionUrl);
const session = response.data;
// Fix for Stalwart behind proxy returning internal docker hostname in apiUrl
const parsedApiUrl = new URL(session.apiUrl);
const parsedBaseUrl = new URL(this.baseUrl);
parsedApiUrl.protocol = parsedBaseUrl.protocol;
parsedApiUrl.host = parsedBaseUrl.host;
parsedApiUrl.port = parsedBaseUrl.port;
this.apiUrl = parsedApiUrl.toString();
console.error("API URL set to:", this.apiUrl);
// Get the primary account for mail
this.accountId = session.primaryAccounts['urn:ietf:params:jmap:mail'];
if (!this.accountId) {
throw new Error("No primary mail account found in JMAP session.");
}
}
catch (error) {
console.error("Failed to connect to JMAP session:", error);
throw error;
}
}
async request(methodCalls) {
if (!this.apiUrl) {
await this.connect();
}
const payload = {
using: [
"urn:ietf:params:jmap:core",
"urn:ietf:params:jmap:mail"
],
methodCalls
};
const response = await this.client.post(this.apiUrl, payload);
return response.data;
}
getAccountId() {
return this.accountId;
}
}
//# sourceMappingURL=jmap_client.js.map

1
build/jmap_client.js.map Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"jmap_client.js","sourceRoot":"","sources":["../jmap_client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAsB,MAAM,OAAO,CAAC;AAElD,MAAM,OAAO,UAAU;IAMP;IACA;IACA;IAPJ,MAAM,GAAW,EAAE,CAAC;IACpB,SAAS,GAAW,EAAE,CAAC;IACvB,MAAM,CAAgB;IAE9B,YACY,OAAe,EACf,QAAgB,EAChB,WAAmB;QAFnB,YAAO,GAAP,OAAO,CAAQ;QACf,aAAQ,GAAR,QAAQ,CAAQ;QAChB,gBAAW,GAAX,WAAW,CAAQ;QAE3B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACpF,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;YACvB,OAAO,EAAE;gBACL,eAAe,EAAE,SAAS,IAAI,EAAE;gBAChC,cAAc,EAAE,kBAAkB;gBAClC,QAAQ,EAAE,kBAAkB;aAC/B;SACJ,CAAC,CAAC;IACP,CAAC;IAED,KAAK,CAAC,OAAO;QACT,gBAAgB;QAChB,wDAAwD;QACxD,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,eAAe,CAAC;QAE/G,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACnD,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC;YAE9B,6EAA6E;YAC7E,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAC7C,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC5C,YAAY,CAAC,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC;YAC/C,YAAY,CAAC,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC;YACvC,YAAY,CAAC,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC;YACvC,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,QAAQ,EAAE,CAAC;YACtC,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAC9C,mCAAmC;YACnC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,eAAe,CAAC,2BAA2B,CAAC,CAAC;YAEtE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;gBAClB,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;YACtE,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,KAAK,CAAC,CAAC;YAC3D,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,WAAoB;QAC9B,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACf,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACzB,CAAC;QAED,MAAM,OAAO,GAAG;YACZ,KAAK,EAAE;gBACH,2BAA2B;gBAC3B,2BAA2B;aAC9B;YACD,WAAW;SACd,CAAC;QAEF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC9D,OAAO,QAAQ,CAAC,IAAI,CAAC;IACzB,CAAC;IAED,YAAY;QACR,OAAO,IAAI,CAAC,SAAS,CAAC;IAC1B,CAAC;CACJ"}