#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import fetch from "node-fetch";
interface IADocumentConfig {
baseUrl: string;
sessionCookie?: string;
}
class IADocumentServer {
private server: Server;
private config: IADocumentConfig;
constructor() {
this.config = {
baseUrl: process.env.IA_BASE_URL || "https://wat-cloud-div.spa-cloud.com",
};
this.server = new Server(
{
name: "ia-document-management",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: this.getTools(),
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "ia_login":
return await this.handleLogin(args);
case "ia_search_documents":
return await this.handleSearchDocuments(args);
case "ia_logout":
return await this.handleLogout(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
}
private getTools(): Tool[] {
return [
{
name: "ia_login",
description:
"Login to iA Document Management System. Returns session information on success.",
inputSchema: {
type: "object",
properties: {
user: {
type: "string",
description: "Username for authentication",
},
password: {
type: "string",
description: "Password for authentication",
},
domain: {
type: "string",
description: "Domain name (defaults to 'local' if omitted)",
default: "local",
},
baseUrl: {
type: "string",
description: "Base URL of the iA Document Management System (optional, uses environment variable if not provided)",
},
},
required: ["user", "password"],
},
},
{
name: "ia_search_documents",
description:
"Search for documents in iA Document Management System using free word search. Requires an active session from ia_login.",
inputSchema: {
type: "object",
properties: {
searchWord: {
type: "string",
description: "Free word search term to search across all document content",
},
folderIds: {
type: "array",
description: "Array of folder IDs to search within",
items: {
type: "object",
properties: {
id: {
type: "string",
description: "Folder ID to search",
},
federationId: {
type: "string",
description: "Optional federation ID",
},
},
required: ["id"],
},
},
operator: {
type: "string",
description: "Operator for combining conditions: 'AND' or 'OR' (defaults to 'AND')",
enum: ["AND", "OR"],
default: "AND",
},
recursive: {
type: "boolean",
description: "Include subfolders in search (defaults to true)",
default: true,
},
properties: {
type: "array",
description: "List of system properties to retrieve (e.g., 'name', 'createDate', 'updateDate')",
items: {
type: "string",
},
},
sessionCookie: {
type: "string",
description: "Session cookie from ia_login response",
},
xsrfToken: {
type: "string",
description: "XSRF token from ia_login response (optional, for CSRF protection)",
},
baseUrl: {
type: "string",
description: "Base URL of the iA Document Management System (optional)",
},
},
required: ["folderIds", "sessionCookie"],
},
},
{
name: "ia_logout",
description:
"Logout from iA Document Management System. Requires an active session.",
inputSchema: {
type: "object",
properties: {
sessionCookie: {
type: "string",
description: "Session cookie from ia_login response",
},
xsrfToken: {
type: "string",
description: "XSRF token from ia_login response (required for CSRF protection)",
},
baseUrl: {
type: "string",
description: "Base URL of the iA Document Management System (optional)",
},
},
required: ["sessionCookie", "xsrfToken"],
},
},
];
}
private async handleLogin(args: any): Promise<any> {
const { user, password, domain = "local", baseUrl } = args;
const targetUrl = baseUrl || this.config.baseUrl;
const params = new URLSearchParams();
params.append("user", user);
params.append("password", password);
params.append("domain", domain);
const response = await fetch(`${targetUrl}/spa/service/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: params.toString(),
});
const errorCode = response.headers.get("X-Spa-Error-Code");
const errorMessage = response.headers.get("X-Spa-Error-Message");
const userId = response.headers.get("X-Spa-User");
const xsrfToken = response.headers.get("X-Xsrf-Token");
// Extract session cookie from Set-Cookie header
const setCookie = response.headers.get("set-cookie");
let sessionCookie = "";
let xsrfCookie = "";
if (setCookie) {
const jsessionMatch = setCookie.match(/JSESSIONID=([^;]+)/);
if (jsessionMatch) {
sessionCookie = jsessionMatch[0];
}
const xsrfMatch = setCookie.match(/XSRF-TOKEN=([^;]+)/);
if (xsrfMatch) {
xsrfCookie = xsrfMatch[0];
}
}
if (response.status === 200 && errorCode === "0") {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
userId: userId ? decodeURIComponent(userId) : null,
sessionCookie,
xsrfCookie,
xsrfToken,
message: "Login successful",
},
null,
2
),
},
],
};
} else {
const decodedMessage = errorMessage
? decodeURIComponent(errorMessage)
: "Login failed";
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: false,
errorCode,
errorMessage: decodedMessage,
status: response.status,
},
null,
2
),
},
],
isError: true,
};
}
}
private async handleSearchDocuments(args: any): Promise<any> {
const {
searchWord,
folderIds,
operator = "AND",
recursive = true,
properties,
sessionCookie,
xsrfToken,
baseUrl,
} = args;
const targetUrl = baseUrl || this.config.baseUrl;
const requestBody: any = {
folderIds,
operator,
recursive,
};
if (searchWord) {
requestBody.searchWord = searchWord;
}
if (properties) {
requestBody.properties = properties;
}
const headers: any = {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Requested-With": "XMLHttpRequest",
Cookie: sessionCookie,
};
if (xsrfToken) {
headers["X-XSRF-TOKEN"] = xsrfToken;
}
const response = await fetch(`${targetUrl}/spa/service/search_v21/folder`, {
method: "POST",
headers,
body: JSON.stringify(requestBody),
});
const errorCode = response.headers.get("X-Spa-Error-Code");
const errorMessage = response.headers.get("X-Spa-Error-Message");
if (response.status === 200) {
const data = await response.json() as any;
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
resultCount: data.resultList?.length || 0,
results: data.resultList || [],
},
null,
2
),
},
],
};
} else {
const decodedMessage = errorMessage
? decodeURIComponent(errorMessage)
: "Search failed";
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: false,
errorCode,
errorMessage: decodedMessage,
status: response.status,
},
null,
2
),
},
],
isError: true,
};
}
}
private async handleLogout(args: any): Promise<any> {
const { sessionCookie, xsrfToken, baseUrl } = args;
const targetUrl = baseUrl || this.config.baseUrl;
const headers: any = {
Cookie: sessionCookie,
"X-Requested-With": "XMLHttpRequest",
};
if (xsrfToken) {
headers["X-XSRF-TOKEN"] = xsrfToken;
}
const response = await fetch(`${targetUrl}/spa/service/auth/logout`, {
method: "POST",
headers,
});
const errorCode = response.headers.get("X-Spa-Error-Code");
const errorMessage = response.headers.get("X-Spa-Error-Message");
const userId = response.headers.get("X-Spa-User");
if (response.status === 200 && errorCode === "0") {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
userId: userId ? decodeURIComponent(userId) : null,
message: "Logout successful",
},
null,
2
),
},
],
};
} else if (response.status === 403) {
// 403 Forbidden - This may occur due to CSRF protection or other security constraints
// The session may still be terminated on the server side
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: false,
status: 403,
warning: "Logout API returned 403 Forbidden. This may be due to CSRF protection restrictions when calling the API outside of a browser context. The session will automatically expire after a period of inactivity. Consider the session as logged out from the client side.",
note: "If you need to ensure the session is terminated, you can try logging out through the web interface at " + (baseUrl || this.config.baseUrl) + "/spa/",
},
null,
2
),
},
],
isError: false, // Not treating as error since this is expected behavior
};
} else {
const decodedMessage = errorMessage
? decodeURIComponent(errorMessage)
: "Logout failed";
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: false,
errorCode,
errorMessage: decodedMessage,
status: response.status,
},
null,
2
),
},
],
isError: true,
};
}
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("iA Document Management MCP Server running on stdio");
}
}
const server = new IADocumentServer();
server.run().catch(console.error);