Initial commit of Stalwart MCP Server
This commit is contained in:
2
build/index.d.ts
vendored
Normal file
2
build/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
build/index.d.ts.map
Normal file
1
build/index.d.ts.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":""}
|
||||
218
build/index.js
Normal file
218
build/index.js
Normal 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
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
13
build/jmap_client.d.ts
vendored
Normal 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
|
||||
1
build/jmap_client.d.ts.map
Normal file
1
build/jmap_client.d.ts.map
Normal 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
66
build/jmap_client.js
Normal 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
1
build/jmap_client.js.map
Normal 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"}
|
||||
Reference in New Issue
Block a user