4 Commits

Author SHA1 Message Date
albnnc bafe299249 w 2026-07-09 09:41:53 +03:00
albnnc 66b1fa0ecc w 2026-07-09 09:41:01 +03:00
albnnc 147b774e44 w 2026-07-09 09:35:22 +03:00
albnnc a79db30ea9 w 2026-07-09 09:21:04 +03:00
11 changed files with 149 additions and 108 deletions
+5
View File
@@ -0,0 +1,5 @@
FROM node:26-slim
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]
+34 -3
View File
@@ -5,7 +5,27 @@ import { createRequire } from "node:module";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { promisify } from "node:util"; import { promisify } from "node:util";
import type { Email, Folder, LoginConfig } from "./models.ts"; import type { Email } from "./types/email.ts";
import type { Folder } from "./types/folder.ts";
import type { LoginConfig } from "./types/login_config.ts";
let inMemoryPassword: string | null = null;
export function setPassword(password: string): void {
inMemoryPassword = password;
}
export function getPassword(): string | null {
return inMemoryPassword;
}
export function hasPassword(): boolean {
return inMemoryPassword !== null;
}
export function clearPassword(): void {
inMemoryPassword = null;
}
const ntlm: any = createRequire(import.meta.url)("httpntlm"); const ntlm: any = createRequire(import.meta.url)("httpntlm");
const postAsync = promisify(ntlm.post.bind(ntlm)); const postAsync = promisify(ntlm.post.bind(ntlm));
@@ -76,27 +96,38 @@ export function clearConfig(): void {
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
} }
clearPassword();
} }
export class EwsClient { export class EwsClient {
private config: LoginConfig; private config: LoginConfig;
private ewsUrl: string; private ewsUrl: string;
constructor(config: LoginConfig) { constructor(config: LoginConfig, password?: string) {
this.config = config; this.config = config;
this.ewsUrl = `${config.serverUrl.replace(/\/+$/, "")}/EWS/Exchange.asmx`; this.ewsUrl = `${config.serverUrl.replace(/\/+$/, "")}/EWS/Exchange.asmx`;
if (password) {
setPassword(password);
}
} }
private get domain(): string { private get domain(): string {
return this.config.domain ?? "corp"; return this.config.domain ?? "corp";
} }
private get password(): string {
const pw = getPassword();
if (!pw) throw new Error("Not logged in. Password not found in memory.");
return pw;
}
private async soapRequest(body: string, soapAction: string): Promise<any> { private async soapRequest(body: string, soapAction: string): Promise<any> {
const res = await postAsync({ const res = await postAsync({
url: this.ewsUrl, url: this.ewsUrl,
username: this.config.username, username: this.config.username,
domain: this.domain, domain: this.domain,
password: this.config.password, password: this.password,
agent: AGENT, agent: AGENT,
headers: { headers: {
"Content-Type": "text/xml; charset=utf-8", "Content-Type": "text/xml; charset=utf-8",
-103
View File
@@ -1,103 +0,0 @@
export interface LoginConfig {
serverUrl: string;
email: string;
username: string;
password: string;
domain?: string;
}
export interface Email {
subject: string;
from: string;
fromName: string;
date: string;
isRead: boolean;
hasAttachments: boolean;
hasLinks: boolean;
itemId: string;
size: number;
isMeeting: boolean;
itemType: string;
to: string[];
cc: string[];
body: string;
bodyType: string;
attachments: Attachment[];
preview: string;
location?: string;
start?: string;
end?: string;
requiredAttendees?: string[];
optionalAttendees?: string[];
}
export interface Attachment {
name: string;
size: number;
contentType: string;
attachmentId: string;
isInline: boolean;
}
export interface Folder {
name: string;
id: string;
totalCount: number;
unreadCount: number;
childFolderCount: number;
}
export interface Person {
name: string;
email: string;
mailboxType: string;
firstName: string;
lastName: string;
jobTitle: string;
department: string;
company: string;
office: string;
alias: string;
manager: string;
managerEmail: string;
phones: Record<string, string>;
address: string;
directReports: Array<{ name: string; email: string }>;
}
export interface CalendarEvent {
subject: string;
start: string;
end: string;
location: string;
isAllDay: boolean;
isCancelled: boolean;
isMeeting: boolean;
isRecurring: boolean;
organizer: string;
organizerEmail: string;
myResponse: string;
itemId: string;
body: string;
requiredAttendees: string[];
optionalAttendees: string[];
}
export interface FreeSlot {
date: string;
start: string;
end: string;
durationMinutes: number;
}
export interface MeetingResult {
success: boolean;
subject: string;
date: string;
startTime: string;
endTime: string;
location: string;
requiredAttendees: string[];
optionalAttendees: string[];
error: string;
}
+14 -2
View File
@@ -3,8 +3,10 @@ import { z } from "zod/v4";
import { import {
clearConfig, clearConfig,
EwsClient, EwsClient,
hasPassword,
loadConfig, loadConfig,
saveConfig, saveConfig,
setPassword,
} from "../ews_client.ts"; } from "../ews_client.ts";
export function registerAuthTools(server: McpServer): void { export function registerAuthTools(server: McpServer): void {
@@ -28,11 +30,10 @@ export function registerAuthTools(server: McpServer): void {
serverUrl: serverUrl.replace(/\/+$/, ""), serverUrl: serverUrl.replace(/\/+$/, ""),
email, email,
username, username,
password,
domain: domain ?? "corp", domain: domain ?? "corp",
}; };
const client = new EwsClient(config); const client = new EwsClient(config, password);
const result = await client.verifyConnection(); const result = await client.verifyConnection();
if (!result.ok) { if (!result.ok) {
@@ -80,6 +81,17 @@ export function registerAuthTools(server: McpServer): void {
}, },
async () => { async () => {
try { try {
if (!hasPassword()) {
return {
content: [{
type: "text" as const,
text: JSON.stringify({
authenticated: false,
error: "Not logged in. Password not found in memory.",
}),
}],
};
}
const config = loadConfig(); const config = loadConfig();
const client = new EwsClient(config); const client = new EwsClient(config);
const result = await client.verifyConnection(); const result = await client.verifyConnection();
+17
View File
@@ -0,0 +1,17 @@
export interface CalendarEvent {
subject: string;
start: string;
end: string;
location: string;
isAllDay: boolean;
isCancelled: boolean;
isMeeting: boolean;
isRecurring: boolean;
organizer: string;
organizerEmail: string;
myResponse: string;
itemId: string;
body: string;
requiredAttendees: string[];
optionalAttendees: string[];
}
+32
View File
@@ -0,0 +1,32 @@
export interface Email {
subject: string;
from: string;
fromName: string;
date: string;
isRead: boolean;
hasAttachments: boolean;
hasLinks: boolean;
itemId: string;
size: number;
isMeeting: boolean;
itemType: string;
to: string[];
cc: string[];
body: string;
bodyType: string;
attachments: Attachment[];
preview: string;
location?: string;
start?: string;
end?: string;
requiredAttendees?: string[];
optionalAttendees?: string[];
}
export interface Attachment {
name: string;
size: number;
contentType: string;
attachmentId: string;
isInline: boolean;
}
+7
View File
@@ -0,0 +1,7 @@
export interface Folder {
name: string;
id: string;
totalCount: number;
unreadCount: number;
childFolderCount: number;
}
+6
View File
@@ -0,0 +1,6 @@
export interface FreeSlot {
date: string;
start: string;
end: string;
durationMinutes: number;
}
+6
View File
@@ -0,0 +1,6 @@
export interface LoginConfig {
serverUrl: string;
email: string;
username: string;
domain?: string;
}
+11
View File
@@ -0,0 +1,11 @@
export interface MeetingResult {
success: boolean;
subject: string;
date: string;
startTime: string;
endTime: string;
location: string;
requiredAttendees: string[];
optionalAttendees: string[];
error: string;
}
+17
View File
@@ -0,0 +1,17 @@
export interface Person {
name: string;
email: string;
mailboxType: string;
firstName: string;
lastName: string;
jobTitle: string;
department: string;
company: string;
office: string;
alias: string;
manager: string;
managerEmail: string;
phones: Record<string, string>;
address: string;
directReports: Array<{ name: string; email: string }>;
}