import { createMCPServer } from "mcp-use/server";
type RequestAuth = {
username: string;
password: string;
projectId: string;
baseUrl: string;
};
type SessionCache = {
token: string;
issuedAt: number;
projectId: string;
projectName: string | null;
lastDocCount: number | null;
baseUrl: string;
};
const sessionCache = new Map<string, SessionCache>();
const connectionAuthCache = new Map<string, RequestAuth>();
const getAuthKey = (username: string, projectId: string) => `${username}:${projectId}`;
const getConnectionId = (c: any): string => {
const ip = c.req.header("x-forwarded-for") || c.req.header("x-real-ip") || "unknown";
const userAgent = c.req.header("user-agent") || "unknown";
return `${ip}:${userAgent}`;
};
const getCachedAuth = (connectionId: string): RequestAuth | null => {
return connectionAuthCache.get(connectionId) || null;
};
const cacheConnectionAuth = (connectionId: string, auth: RequestAuth) => {
connectionAuthCache.set(connectionId, auth);
};
const getAuthFromRecentHistory = (): RequestAuth | null => {
for (let i = mcpRequestHistory.length - 1; i >= 0; i--) {
const req = mcpRequestHistory[i];
const username = req.headers["username"] || req.headers["Username"];
const password = req.headers["password"] || req.headers["Password"];
const projectId = req.headers["projectid"] || req.headers["ProjectId"] || req.headers["project-id"];
const baseUrl = req.headers["torna-base-url"] || req.headers["Torna-Base-Url"] || req.headers["baseurl"] || req.headers["BaseUrl"];
if (username && password && projectId) {
return { username, password, projectId, baseUrl };
}
}
return null;
};
const getAuthForTool = (): RequestAuth | null => {
return getAuthFromRecentHistory();
};
const resolveBaseUrl = (customBaseUrl: string) => {
if (!customBaseUrl) {
throw new Error("baseUrl is required. Please provide it via request header 'torna-base-url' or 'baseurl'");
}
return customBaseUrl.replace(/\/$/, "");
};
const getAuthFromHeaders = (c: any): RequestAuth | null => {
const username = c.req.header("username");
const password = c.req.header("password");
const projectId = c.req.header("projectid");
const baseUrl = c.req.header("torna-base-url") || c.req.header("baseurl");
if (!username || !password || !projectId || !baseUrl) {
return null;
}
return { username, password, projectId, baseUrl };
};
type ProxyOptions = {
searchParams?: Record<string, string | undefined>;
body?: Record<string, any>;
token?: string;
};
type ProxyResponse = {
status: number;
ok: boolean;
data: any;
};
const proxyTornaJson = async (
label: string,
method: string,
path: string,
options: ProxyOptions = {},
customBaseUrl: string
): Promise<ProxyResponse> => {
const base = resolveBaseUrl(customBaseUrl);
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
const url = new URL(base + normalizedPath);
if (options.searchParams) {
for (const [key, value] of Object.entries(options.searchParams)) {
if (value !== undefined && value !== null) {
url.searchParams.set(key, value);
}
}
}
const headers: Record<string, string> = { Accept: "application/json" };
let body: string | undefined;
if (options.body) {
headers["Content-Type"] = "application/json";
body = JSON.stringify(options.body);
}
if (options.token) {
headers["token"] = options.token;
}
console.log(`[TornaExternal] request`, {
label,
method,
url: url.toString(),
body: options.body ?? null,
});
const response = await fetch(url, {
method,
headers,
body,
});
const responseText = await response.text();
let data: any;
try {
data = responseText ? JSON.parse(responseText) : null;
} catch {
data = responseText;
}
console.log(`[TornaExternal] response`, {
label,
method,
url: url.toString(),
status: response.status,
data,
});
return {
status: response.status,
ok: response.ok,
data,
};
};
const fetchProjectDocs = async (projectId: string, token: string, session: SessionCache) => {
const response = await proxyTornaJson("dataByProject", "GET", "/doc/view/dataByProject", {
token,
searchParams: { projectId },
}, session.baseUrl!);
if (Array.isArray(response.data?.data)) {
session.lastDocCount = response.data.data.length;
}
return response;
};
const performLogin = async (username: string, password: string, projectId: string, customBaseUrl: string): Promise<SessionCache> => {
const authKey = getAuthKey(username, projectId);
const cached = sessionCache.get(authKey);
if (cached && cached.issuedAt && Date.now() - cached.issuedAt < 3600000 && cached.baseUrl === customBaseUrl) {
return cached;
}
const proxyResponse = await proxyTornaJson("login", "POST", "/system/login", {
body: {
username,
password,
source: "mcp",
},
}, customBaseUrl);
const token = proxyResponse.data?.data?.token;
if (!proxyResponse.ok || !token) {
throw new Error(
proxyResponse.data?.msg || `Torna login failed with status ${proxyResponse.status}`
);
}
const session: SessionCache = {
token,
issuedAt: Date.now(),
projectId: proxyResponse.data?.data?.projectId || projectId,
projectName: proxyResponse.data?.data?.projectName || null,
lastDocCount: null,
baseUrl: customBaseUrl,
};
sessionCache.set(authKey, session);
return session;
};
const requireSession = async (c: any): Promise<SessionCache> => {
const auth = getAuthFromHeaders(c);
if (!auth) {
throw new Error("缺少认证信息:请在请求头中提供 username、password、projectId、torna-base-url");
}
return await performLogin(auth.username, auth.password, auth.projectId, auth.baseUrl!);
};
const fetchDocDetail = async (docId: string, token: string, customBaseUrl: string) => {
const response = await proxyTornaJson("detail", "GET", "/doc/view/detail", {
token,
searchParams: { id: docId },
}, customBaseUrl);
if (!response.ok || !response.data?.data) {
throw new Error(response.data?.msg || `Torna doc ${docId} not found`);
}
return response.data.data;
};
const fetchProjects = async (token: string, customBaseUrl: string) => {
const response = await proxyTornaJson("projects", "GET", "/doc/view/projects", {
token,
}, customBaseUrl);
return response.data;
};
const selectProject = (projectId: string, projects: any, session: SessionCache) => {
let name: string | null = null;
const list = projects?.data ?? [];
for (const space of list) {
const match = space?.projects?.find?.((proj: any) => proj?.id === projectId);
if (match) {
name = match.name || null;
break;
}
}
session.projectId = projectId;
session.projectName = name;
};
const buildFullPathLabel = (entry: any, docs: any[]): string => {
if (!entry || !entry.label) return "";
if (!entry.parentId || entry.parentId === "") return entry.label;
const path: string[] = [entry.label];
let current: any = entry;
const visited = new Set<string>();
while (current && current.parentId && current.parentId !== "") {
if (visited.has(current.id)) break;
visited.add(current.id);
const parent = docs.find((d: any) => d.id === current.parentId);
if (!parent) break;
if (parent.label) {
path.unshift(parent.label);
}
current = parent;
}
return path.join("/");
};
// Create MCP server instance
const server = createMCPServer("my-mcp-server", {
version: "1.0.0",
description: "My first MCP server with all features",
baseUrl: process.env.MCP_URL || "http://localhost:3000", // Full base URL (e.g., https://myserver.com)
});
// Storage for /mcp endpoint parameters
type MCPRequestParams = {
timestamp: number;
method: string;
headers: Record<string, string>;
queryParams: Record<string, string>;
url: string;
};
const mcpRequestHistory: MCPRequestParams[] = [];
const MAX_HISTORY_SIZE = 1000;
// Middleware to capture /mcp endpoint parameters and cache authentication
server.use("/mcp", async (c, next) => {
const connectionId = getConnectionId(c);
const auth = getAuthFromHeaders(c);
if (auth) {
cacheConnectionAuth(connectionId, auth);
try {
await performLogin(auth.username, auth.password, auth.projectId, auth.baseUrl);
console.log(`[MCPAuth] Cached authentication for connection: ${connectionId}`);
} catch (error: any) {
console.warn(`[MCPAuth] Failed to login for connection ${connectionId}:`, error.message);
}
}
const headers: Record<string, string> = {};
try {
if (c.req.raw?.headers) {
const rawHeaders = c.req.raw.headers;
if (rawHeaders instanceof Headers) {
rawHeaders.forEach((value, key) => {
headers[key] = value;
});
} else {
Object.entries(rawHeaders).forEach(([key, value]) => {
headers[key] = Array.isArray(value) ? value.join(", ") : String(value);
});
}
}
} catch (e) {
console.warn(`[MCPCapture] Failed to extract headers:`, e);
}
const queryParams: Record<string, string> = {};
try {
const urlStr = c.req.url;
if (urlStr) {
const url = urlStr.startsWith("http")
? new URL(urlStr)
: new URL(urlStr, `http://${c.req.header("host") || "localhost"}`);
url.searchParams.forEach((value, key) => {
queryParams[key] = value;
});
}
} catch (e) {
console.warn(`[MCPCapture] Failed to extract query params:`, e);
}
const params: MCPRequestParams = {
timestamp: Date.now(),
method: c.req.method,
headers,
queryParams,
url: c.req.url,
};
mcpRequestHistory.push(params);
if (mcpRequestHistory.length > MAX_HISTORY_SIZE) {
mcpRequestHistory.shift();
}
console.log(`[MCPCapture] /mcp request captured:`, {
timestamp: new Date(params.timestamp).toISOString(),
method: params.method,
url: params.url,
headersCount: Object.keys(headers).length,
queryParamsCount: Object.keys(queryParams).length,
hasAuth: !!auth,
connectionId,
headers,
queryParams,
});
await next();
});
/**
* UI Widgets (optional)
* Drop React components into `resources/` and export widgetMetadata
* to have mcp-use auto-register corresponding tools/resources.
* (This starter currently ships without any UI widgets.)
*/
/*
* Define MCP tools
* Docs: https://docs.mcp-use.com/typescript/server/tools
*/
server.tool({
name: "torna-project-info",
description: "Return the current Torna project context and token state. Authentication is automatically retrieved from connection headers.",
inputs: [],
cb: async () => {
const auth = getAuthForTool();
if (!auth) {
throw new Error("缺少认证信息:请在连接时通过 HTTP 请求头提供 username、password、projectId");
}
const session = await performLogin(auth.username, auth.password, auth.projectId, auth.baseUrl!);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
username: auth.username,
projectId: session.projectId,
token: session.token,
tokenIssuedAt: new Date(session.issuedAt).toISOString(),
isLoggedIn: true,
lastDocCount: session.lastDocCount,
projectName: session.projectName,
baseUrl: session.baseUrl,
},
null,
2
),
},
],
};
},
});
server.tool({
name: "torna-search-docs",
description:
"Search Torna project documents by label or URL. When no keyword is provided, fetches the full project tree and returns it. Authentication is automatically retrieved from connection headers.",
inputs: [
{ name: "keyword", type: "string", required: false },
],
cb: async (params: Record<string, any>) => {
const auth = getAuthForTool();
if (!auth) {
throw new Error("缺少认证信息:请在连接时通过 HTTP 请求头提供 username、password、projectId");
}
const session = await performLogin(auth.username, auth.password, auth.projectId, auth.baseUrl!);
const keyword = typeof params.keyword === "string" ? params.keyword.trim() : "";
const treeResponse = await fetchProjectDocs(session.projectId, session.token, session);
const docs = Array.isArray(treeResponse.data?.data) ? treeResponse.data.data : [];
const normalized = keyword.toLowerCase();
const filtered = normalized
? docs.filter((entry: any) => {
if (entry?.type !== 3) return false;
const label = entry?.label?.toLowerCase?.() ?? "";
const url = entry?.url?.toLowerCase?.() ?? "";
const fullPath = buildFullPathLabel(entry, docs).toLowerCase();
return label.includes(normalized) || url.includes(normalized) || fullPath.includes(normalized);
})
: docs.filter((entry: any) => entry?.type === 3);
const payload = {
code: treeResponse.data?.code ?? "0",
msg: treeResponse.data?.msg ?? "",
projectId: session.projectId,
keyword: keyword || null,
count: filtered.length,
data: filtered,
};
return {
content: [
{
type: "text",
text: JSON.stringify(payload, null, 2),
},
],
};
},
});
server.tool({
name: "torna-get-doc-detail",
description: "Retrieve detailed Torna document definition(s). Supports single docId or comma-separated multiple docIds for batch retrieval. Authentication is automatically retrieved from connection headers.",
inputs: [
{ name: "docId", type: "string", required: true, description: "Single docId or comma-separated list of docIds" },
],
cb: async (params: Record<string, any>) => {
const auth = getAuthForTool();
if (!auth) {
throw new Error("缺少认证信息:请在连接时通过 HTTP 请求头提供 username、password、projectId");
}
const session = await performLogin(auth.username, auth.password, auth.projectId, auth.baseUrl!);
const docIdInput = typeof params.docId === "string" ? params.docId.trim() : "";
if (!docIdInput) {
throw new Error("docId is required");
}
const docIds = docIdInput
.split(",")
.map((id) => id.trim())
.filter((id) => id.length > 0);
if (docIds.length === 0) {
throw new Error("At least one docId is required");
}
if (docIds.length === 1) {
const detail = await fetchDocDetail(docIds[0], session.token, session.baseUrl!);
return {
content: [{ type: "text", text: JSON.stringify({ code: "0", data: detail }, null, 2) }],
};
}
const details = await Promise.all(
docIds.map(async (docId) => {
try {
const detail = await fetchDocDetail(docId, session.token, session.baseUrl!);
return { docId, success: true, data: detail };
} catch (error: any) {
return { docId, success: false, error: error.message };
}
})
);
const successCount = details.filter((d) => d.success).length;
const failCount = details.length - successCount;
return {
content: [
{
type: "text",
text: JSON.stringify(
{
code: "0",
total: docIds.length,
success: successCount,
failed: failCount,
data: details,
},
null,
2
),
},
],
};
},
});
server.tool({
name: "torna-list-projects",
description: "List all spaces/projects visible to the Torna session. Authentication is automatically retrieved from connection headers.",
inputs: [],
cb: async () => {
const auth = getAuthForTool();
if (!auth) {
throw new Error("缺少认证信息:请在连接时通过 HTTP 请求头提供 username、password、projectId");
}
const session = await performLogin(auth.username, auth.password, auth.projectId, auth.baseUrl!);
const projects = await fetchProjects(session.token, session.baseUrl!);
if (!session.projectName && projects?.data) {
selectProject(session.projectId, projects, session);
}
return {
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
};
},
});
server.tool({
name: "torna-select-project",
description: "Select a specific Torna project ID for subsequent requests. Authentication is automatically retrieved from connection headers.",
inputs: [
{ name: "projectId", type: "string", required: true },
],
cb: async (params: Record<string, any>) => {
const auth = getAuthForTool();
if (!auth) {
throw new Error("缺少认证信息:请在连接时通过 HTTP 请求头提供 username、password、projectId");
}
const session = await performLogin(auth.username, auth.password, auth.projectId, auth.baseUrl!);
const projectId = typeof params.projectId === "string" ? params.projectId.trim() : "";
if (!projectId) {
throw new Error("projectId is required");
}
const projects = await fetchProjects(session.token, session.baseUrl!);
const list = projects?.data ?? [];
const exists = list.some((space: any) =>
space?.projects?.some?.((proj: any) => proj?.id === projectId)
);
if (!exists) {
throw new Error(`Project ${projectId} not found in available list`);
}
const newSession = await performLogin(auth.username, auth.password, projectId, auth.baseUrl!);
selectProject(projectId, projects, newSession);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
code: "0",
msg: "project updated",
projectId: newSession.projectId,
projectName: newSession.projectName,
},
null,
2
),
},
],
};
},
});
/*
* Define MCP resources
* Docs: https://docs.mcp-use.com/typescript/server/resources
*/
server.resource({
name: "config",
uri: "config://settings",
mimeType: "application/json",
description: "Server configuration",
readCallback: async () => ({
contents: [
{
uri: "config://settings",
mimeType: "application/json",
text: JSON.stringify({
theme: "dark",
language: "en",
}),
},
],
}),
});
server.resource({
name: "torna-session-project",
uri: "torna://session/project",
mimeType: "application/json",
description: "Current Torna session project info. Note: This resource requires authentication via HTTP request headers (username, password, projectid).",
readCallback: async () => ({
contents: [
{
uri: "torna://session/project",
mimeType: "application/json",
text: JSON.stringify(
{
error: "This resource requires authentication. Please use HTTP endpoints with request headers (username, password, projectid) or use MCP tools with authentication inputs.",
},
null,
2
),
},
],
}),
});
server.resource({
name: "torna-projects",
uri: "torna://projects",
mimeType: "application/json",
description: "List of Torna spaces/projects accessible by this session.",
readCallback: async () => {
const auth = getAuthFromRecentHistory();
if (!auth) {
return {
contents: [
{
uri: "torna://projects",
mimeType: "application/json",
text: JSON.stringify({
error: "This resource requires authentication. Please use HTTP endpoints with request headers (username, password, projectid) or use MCP tools with authentication inputs.",
}, null, 2),
},
],
};
}
const session = await performLogin(auth.username, auth.password, auth.projectId, auth.baseUrl!);
const projects = await fetchProjects(session.token, session.baseUrl!);
if (!session.projectName && projects?.data) {
selectProject(session.projectId, projects, session);
}
return {
contents: [
{
uri: "torna://projects",
mimeType: "application/json",
text: JSON.stringify(projects, null, 2),
},
],
};
},
});
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
console.log(`Server running on port ${PORT}`);
// Start the server
server.listen(PORT);
/*
* Define MCP prompts
*/
const requireDocId = (params: Record<string, any>) => {
const docId = typeof params.docId === "string" ? params.docId.trim() : "";
if (!docId) {
throw new Error("docId is required");
}
return docId;
};
server.prompt({
name: "torna-find-apis-by-need",
description: "Find Torna APIs that can fulfill a given requirement or need described in natural language.",
args: [{ name: "requirement", type: "string", required: true }],
cb: async (params: Record<string, any>) => {
const auth = getAuthFromRecentHistory();
if (!auth) {
throw new Error("缺少认证信息:请在首次连接时通过 HTTP 请求头提供 username、password、projectId、torna-base-url");
}
const session = await performLogin(auth.username, auth.password, auth.projectId, auth.baseUrl!);
const requirement = typeof params.requirement === "string" ? params.requirement.trim() : "";
if (!requirement) {
throw new Error("requirement is required");
}
const projectId = session.projectId;
const treeResponse = await fetchProjectDocs(projectId, session.token, session);
const docs = Array.isArray(treeResponse.data?.data) ? treeResponse.data.data : [];
const apiDocs = docs.filter((entry: any) => entry?.type === 3);
const apiList = apiDocs.map((entry: any) => ({
docId: entry.id,
name: entry.label,
url: entry.url,
fullPath: buildFullPathLabel(entry, docs),
}));
if (apiList.length === 0) {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `No APIs found in the current project. The project might be empty or there was an error fetching the API list.`,
},
},
],
};
}
const MAX_APIS_FOR_DETAIL = 30;
let apiListWithDetails = apiList;
if (apiList.length <= MAX_APIS_FOR_DETAIL) {
const details = await Promise.all(
apiList.map(async (api: any) => {
try {
const detail = await fetchDocDetail(api.docId, session.token, session.baseUrl!);
return {
...api,
httpMethod: detail?.httpMethod || null,
description: detail?.description || detail?.remark || null,
requestParams: Array.isArray(detail?.requestParams)
? detail.requestParams
.filter((p: any) => p?.required)
.map((p: any) => ({
name: p?.name,
type: p?.dataType,
desc: p?.description,
}))
.slice(0, 5)
: null,
responseFields: Array.isArray(detail?.responseParams)
? detail.responseParams
.slice(0, 10)
.map((p: any) => ({
name: p?.name,
type: p?.dataType,
desc: p?.description,
}))
: null,
};
} catch {
return api;
}
})
);
apiListWithDetails = details;
}
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `User requirement: "${requirement}"
Analyze the following Torna API list and identify which APIs can fulfill this requirement. Consider the API name, URL path, HTTP method, description, request parameters, and response structure when matching.
Available APIs (${apiList.length} total${apiList.length <= MAX_APIS_FOR_DETAIL ? " with detailed information" : ", showing basic info only"}):
${JSON.stringify(apiListWithDetails, null, 2)}
Return a JSON array of matching APIs. For each match, include:
- docId: document ID
- name: API name
- url: API endpoint
- fullPath: full path in the API tree
- reason: why this API matches the requirement (be specific about which fields helped in matching)
If no APIs match, return an empty array [].
Return ONLY the JSON array, no other text.`,
},
},
],
};
},
});
server.prompt({
name: "torna-generate-request",
description: "Generate an HTTP request example for a Torna document (curl-style).",
args: [{ name: "docId", type: "string", required: true }],
cb: async (params: Record<string, any>) => {
const auth = getAuthFromRecentHistory();
if (!auth) {
throw new Error("缺少认证信息:请在首次连接时通过 HTTP 请求头提供 username、password、projectId、torna-base-url");
}
const session = await performLogin(auth.username, auth.password, auth.projectId, auth.baseUrl!);
const docId = requireDocId(params);
const detail = await fetchDocDetail(docId, session.token, session.baseUrl);
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `You are a Torna API assistant. Produce a curl command for method ${detail?.httpMethod} ${detail?.url}. Include sample headers and placeholder values for required request parameters.\n\n${JSON.stringify(detail, null, 2)}`,
},
},
],
};
},
});