Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bf8eda2

Browse files
committed
keep track of sessions for chat
1 parent f94e541 commit bf8eda2

File tree

8 files changed

+186
-14
lines changed

8 files changed

+186
-14
lines changed

README.md

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Integration App MCP Server
2-
<img width="1148" alt="Screenshot 2025-07-07 at 23 03 05" src="https://github.com/user-attachments/assets/39f6cc74-a689-4657-91f3-ee8358c05e31" />
32

3+
<img width="1148" alt="Screenshot 2025-07-07 at 23 03 05" src="https://github.com/user-attachments/assets/39f6cc74-a689-4657-91f3-ee8358c05e31" />
44

55
The Integration App MCP Server is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server, it provides actions for connected integrations on Integration.app membrane as tools.
66

@@ -107,13 +107,15 @@ await client.connect(
107107
);
108108
```
109109

110-
### ⚡ Static & Dynamic Mode
110+
### ⚡ Static vs Dynamic Mode
111+
112+
By default, the MCP server runs in **static mode**, which means it returns **all available tools** (actions) for all connected integrations.
111113

112-
By default, the MCP server is in `static` mode and will return all tools. In `dynamic` mode (`?mode=dynamic`) the MCP server will only return only a single tool: `enable-tools`, you can use this tool to enable tools for the session.
114+
If you switch to **dynamic mode** (by adding `?mode=dynamic` to your request), the server will only return **one tool**: `enable-tools`. You can use this tool to selectively enable the tools you actually need for that session.
113115

114-
Your implementation needs to provide a way to find the most relevant tools to the user query, after which you can use the `enable-tools` tool to enable the tools for the session. Ideally you want to prompt LLM to call this tool
116+
In dynamic mode, your implementation should figure out which tools are most relevant to the user's query. Once you've identified them, prompt the LLM to call the `enable-tools` tool with the appropriate list.
115117

116-
See an example implementation in our [AI Agent Example](https://github.com/integration-app/ai-agent-example)
118+
Want to see how this works in practice? Check out our [AI Agent Example](https://github.com/integration-app/ai-agent-example).
117119

118120
```ts
119121
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -151,6 +153,36 @@ In static mode, the MCP server fetches tools from all active connections associa
151153

152154
You can choose to only fetch tools for a specific integration by passing the `apps` query parameter: `/mcp?apps=google-calendar,google-docs`
153155

156+
### 💬 Chat Session Management
157+
158+
The MCP server supports persistent chat sessions. Include an `x-chat-id` header in your requests to automatically track sessions for that specific chat.
159+
160+
**Starting a new chat session:**
161+
162+
```http
163+
POST /mcp
164+
Authorization: Bearer YOUR_ACCESS_TOKEN
165+
x-chat-id: my-awesome-chat-123
166+
```
167+
168+
**Retrieving your chat sessions:**
169+
170+
```http
171+
GET /mcp/sessions
172+
Authorization: Bearer YOUR_ACCESS_TOKEN
173+
```
174+
175+
**Response:**
176+
177+
```json
178+
{
179+
"my-awesome-chat-123": "session-uuid-1",
180+
"another-chat-456": "session-uuid-2"
181+
}
182+
```
183+
184+
This feature lets you maintain conversation context across multiple requests without creating new sessions each time. Perfect for multi-chat applications! Check out our [AI Agent Example](https://github.com/integration-app/ai-agent-example) to see how this works in practice.
185+
154186
### Configuring other MCP clients
155187

156188
#### 📝 Cursor

src/server/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ app.use(
2222
cors({
2323
origin: '*',
2424
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
25-
allowedHeaders: ['Content-Type', 'Authorization', 'mcp-session-id', 'last-event-id'],
25+
allowedHeaders: ['Content-Type', 'Authorization', 'mcp-session-id', 'x-chat-id', 'last-event-id'],
2626
})
2727
);
2828
app.use(morgan(customMorganFormat));

src/server/middlewares/logging.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ export const customMorganFormat = (tokens: any, req: any, res: any) => {
1313
}
1414

1515
const sessionId = req.headers['mcp-session-id'];
16+
const chatId = req.headers['x-chat-id'];
1617
const userId = req.userId;
1718

1819
return `
19-
${colorize(method, getMethodColor(method))} ${colorize(truncatedUrl, colors.white)} ${colorize(status, getStatusColor(status))} ${colorize(`${responseTime}ms`, colors.gray)} 👤 User: ${colorize(userId, colors.green)} Session: ${colorize(sessionId, colors.cyan)} 🔧 Method: ${colorize(req.body.method, colors.magenta)} Mode: ${colorize(req.query.mode, colors.yellow)}`;
20+
${colorize(method, getMethodColor(method))} ${colorize(truncatedUrl, colors.white)} ${colorize(status, getStatusColor(status))} ${colorize(`${responseTime}ms`, colors.gray)} 👤 User: ${colorize(userId, colors.green)} 💬 Chat: ${colorize(chatId, colors.blue)} 🔑 Session: ${colorize(sessionId, colors.cyan)} 🔧 Method: ${colorize(req.body.method, colors.magenta)}`;
2021
};

src/server/routes/streamable-http.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { randomUUID } from 'crypto';
33
import express, { Request, Response } from 'express';
44
import { createMcpServer, CreateMcpServerParams } from '../utils/create-mcp-server';
55
import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js';
6+
import { addToUserSessions, getUserSessions, removeFromUserSessions } from '../utils/user-sessions';
67

78
export const streamableHttpRouter = express.Router();
89

@@ -34,6 +35,11 @@ streamableHttpRouter.post('/', async (req, res) => {
3435
// Store the transport by session ID when session is initialized
3536
// This avoids race conditions where requests might come in before the session is stored
3637
transports.set(sessionId, transport);
38+
39+
const userId = req.userId;
40+
const chatId = req.headers['x-chat-id'] as string | undefined;
41+
42+
addToUserSessions({ userId, chatId, sessionId });
3743
},
3844
});
3945

@@ -145,6 +151,11 @@ streamableHttpRouter.delete('/', async (req: Request, res: Response) => {
145151
try {
146152
const transport = transports.get(sessionId);
147153
await transport!.handleRequest(req, res);
154+
155+
const userId = req.userId;
156+
const chatId = req.headers['x-chat-id'] as string | undefined;
157+
158+
removeFromUserSessions({ userId, chatId, sessionId });
148159
} catch (error) {
149160
console.error('Error handling session termination:', error);
150161
if (!res.headersSent) {
@@ -160,3 +171,11 @@ streamableHttpRouter.delete('/', async (req: Request, res: Response) => {
160171
}
161172
}
162173
});
174+
175+
// Get all the sessions user has opened
176+
streamableHttpRouter.get('/sessions', async (req: Request, res: Response) => {
177+
const userId = req.userId;
178+
const sessions = getUserSessions(userId);
179+
180+
res.json(sessions);
181+
});

src/server/utils/create-mcp-server.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { isTestEnvironment } from './constants';
66
import { zodFromJsonSchema } from './json-schema-to-zod';
77
import { ZodRawShape } from 'zod';
88
import pkg from '../../../package.json';
9+
import { getActionsByKeys } from './membrane/get-actions-by-keys';
910

1011
/**
1112
* Register a tool to the MCP server while adding some standardization
@@ -81,11 +82,20 @@ export const createMcpServer = async ({
8182
apps,
8283
mode = 'static',
8384
}: CreateMcpServerParams) => {
84-
const mcpServer = new McpServer({
85-
name: 'Integration App MCP Server',
86-
version: pkg.version,
87-
description: pkg.description,
88-
});
85+
const mcpServer = new McpServer(
86+
{
87+
name: 'Integration App MCP Server',
88+
version: pkg.version,
89+
description: pkg.description,
90+
},
91+
{
92+
capabilities: {
93+
tools: {
94+
listChanged: true,
95+
},
96+
},
97+
}
98+
);
8999

90100
let membrane: IntegrationAppClient;
91101

@@ -140,8 +150,39 @@ export const createMcpServer = async ({
140150

141151
// Enable new tools
142152
for (const tool of args.tools) {
143-
registeredTools[tool]?.enable();
144-
enabledTools.push(tool);
153+
/**
154+
* There are cases where the tool client is trying to enable is not a registered tool.
155+
* In that case, we need to get the action from membrane, register it and enable it.
156+
*/
157+
const isToolRegistered = registeredTools[tool];
158+
159+
if (isToolRegistered) {
160+
registeredTools[tool].enable();
161+
enabledTools.push(tool);
162+
} else {
163+
const actions = await getActionsByKeys(
164+
membrane,
165+
args.tools.map((toolKey: string) => {
166+
const [integrationKey, actionKey] = toolKey.split('_');
167+
168+
return {
169+
key: actionKey,
170+
integrationKey,
171+
};
172+
})
173+
);
174+
175+
for (const action of actions) {
176+
if (action) {
177+
const addedTool = addServerTool({ mcpServer, action, membrane });
178+
if (addedTool) {
179+
registeredTools[addedTool.toolName] = addedTool.tool;
180+
addedTool.tool.enable();
181+
enabledTools.push(addedTool.toolName);
182+
}
183+
}
184+
}
185+
}
145186
}
146187

147188
return {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { IntegrationAppClient } from '@integration-app/sdk';
2+
3+
export const getActionsByKeys = async (
4+
membrane: IntegrationAppClient,
5+
_actions: {
6+
key: string;
7+
integrationKey: string;
8+
}[]
9+
) => {
10+
const results = await Promise.allSettled(
11+
_actions.map(async action => {
12+
return membrane
13+
.action({
14+
integrationKey: action.integrationKey,
15+
key: action.key,
16+
})
17+
.get();
18+
})
19+
);
20+
21+
// Filter out failed promises and return only successful results
22+
const successfulActions = results
23+
.filter((result): result is PromiseFulfilledResult<any> => result.status === 'fulfilled')
24+
.map(result => result.value);
25+
26+
return successfulActions;
27+
};

src/server/utils/user-sessions.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Stores user sessions per chat
3+
*
4+
* @example
5+
* {
6+
* "userId": {
7+
* "chatId": "sessionId",
8+
* "chatId2": "sessionId2",
9+
* }
10+
* }
11+
*/
12+
13+
const userSessions: Record<string, Record<string, string>> = {};
14+
15+
export const addToUserSessions = ({
16+
userId,
17+
chatId,
18+
sessionId,
19+
}: {
20+
userId: string;
21+
chatId?: string;
22+
sessionId: string;
23+
}) => {
24+
if (chatId) {
25+
const currentSessionsForUser = userSessions[userId] ?? {};
26+
userSessions[userId] = {
27+
...currentSessionsForUser,
28+
[chatId]: sessionId,
29+
};
30+
}
31+
};
32+
33+
export const removeFromUserSessions = ({
34+
userId,
35+
chatId,
36+
sessionId,
37+
}: {
38+
userId: string;
39+
chatId?: string;
40+
sessionId: string;
41+
}) => {
42+
const currentSessionsForUser = userSessions[userId];
43+
if (currentSessionsForUser) {
44+
delete currentSessionsForUser[chatId ?? sessionId];
45+
userSessions[userId] = currentSessionsForUser;
46+
}
47+
};
48+
49+
export const getUserSessions = (userId: string) => {
50+
return userSessions[userId];
51+
};

tests/mcp-server.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ describe('MCP Server Integration Tests', () => {
5353
requestInit: {
5454
headers: {
5555
Authorization: `Bearer ${TEST_USER_ACCESS_TOKEN}`,
56+
'x-chat-id': TEST_CHAT_ID,
5657
},
5758
},
5859
});

0 commit comments

Comments
 (0)