Skip to content

Commit b76912e

Browse files
klavis mcp integrated (#52)
* klavis mcp integrated * Update packages/tools/src/klavis/KlavisMCPTools.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update packages/tools/src/klavis/KlavisMCPTools.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update packages/tools/src/klavis/KlavisMCPTools.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent 4b5e8ec commit b76912e

File tree

10 files changed

+739
-4
lines changed

10 files changed

+739
-4
lines changed

packages/agent/src/session/SessionManager.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ enum SessionState {
2828
*/
2929
const SessionSchema = z.object({
3030
id: z.string().uuid(),
31+
userId: z.string().optional(), // Klavis user ID for MCP integration
3132
state: z.nativeEnum(SessionState),
3233
createdAt: z.number().positive(),
3334
lastActivity: z.number().positive(),
@@ -54,6 +55,7 @@ type SessionMetrics = z.infer<typeof SessionMetricsSchema>;
5455
*/
5556
const CreateSessionOptionsSchema = z.object({
5657
id: z.string().uuid().optional(), // Optional: specify sessionId (useful for testing)
58+
userId: z.string().optional(), // Optional: Klavis user ID for MCP integration
5759
agentType: z.string().min(1).optional(), // Optional: agent type (defaults to 'codex-sdk')
5860
});
5961

@@ -122,6 +124,7 @@ export class SessionManager {
122124

123125
const session: Session = {
124126
id: sessionId,
127+
userId: options?.userId,
125128
state: SessionState.IDLE,
126129
createdAt: now,
127130
lastActivity: now,
@@ -195,6 +198,16 @@ export class SessionManager {
195198
return this.agents.get(sessionId);
196199
}
197200

201+
/**
202+
* Get user ID for a session
203+
*
204+
* @param sessionId - Session ID
205+
* @returns User ID or undefined if not set
206+
*/
207+
getUserId(sessionId: string): string | undefined {
208+
return this.sessions.get(sessionId)?.userId;
209+
}
210+
198211
/**
199212
* Update session activity timestamp
200213
*/

packages/mcp/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"declaration": true,
88
"declarationMap": true
99
},
10-
"include": ["src/**/*", "tests/**/*"],
10+
"include": ["src/**/*", "tests/**/*", "../tools/src/klavis"],
1111
"exclude": ["node_modules", "dist/**/*"],
1212
"references": [{"path": "../common"}, {"path": "../tools"}]
1313
}

packages/server/src/main.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
allControllerTools,
3131
type ToolDefinition,
3232
} from '@browseros/tools';
33+
import {allKlavisTools} from '@browseros/tools/klavis';
3334

3435
import {parseArguments} from './args.js';
3536

@@ -135,13 +136,14 @@ function mergeTools(
135136
allControllerTools,
136137
controllerContext,
137138
);
139+
const klavisTools = process.env.KLAVIS_API_KEY ? allKlavisTools : [];
138140

139141
logger.info(
140-
`Total tools available: ${cdpTools.length + wrappedControllerTools.length} ` +
141-
`(${cdpTools.length} CDP + ${wrappedControllerTools.length} extension)`,
142+
`Total tools available: ${cdpTools.length + wrappedControllerTools.length + klavisTools.length} ` +
143+
`(${cdpTools.length} CDP + ${wrappedControllerTools.length} extension + ${klavisTools.length} Klavis)`,
142144
);
143145

144-
return [...cdpTools, ...wrappedControllerTools];
146+
return [...cdpTools, ...wrappedControllerTools, ...klavisTools];
145147
}
146148

147149
function startMcpServer(config: {

packages/tools/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
".": "./src/index.ts",
99
"./cdp-based": "./src/cdp-based/index.ts",
1010
"./controller-based": "./src/controller-based/index.ts",
11+
"./klavis": "./src/klavis/index.ts",
1112
"./response": "./src/response/index.ts",
1213
"./formatters": "./src/formatters/index.ts",
1314
"./types": "./src/types/index.ts"

packages/tools/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export * as cdpTools from './cdp-based/index.js';
1313
export {allControllerTools} from './controller-based/index.js';
1414
export * as controllerTools from './controller-based/index.js';
1515

16+
// Export Klavis MCP tools (Gmail, Google Calendar, Sheets, Docs, Notion, etc.)
17+
export {allKlavisTools} from './klavis/index.js';
18+
export * as klavisTools from './klavis/index.js';
19+
1620
// Export types
1721
export * from './types/index.js';
1822

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/**
2+
* Minimal Klavis API client for MCP server operations
3+
* No external dependencies - just fetch API and TypeScript
4+
*/
5+
6+
// Simple type definitions for API responses
7+
export interface UserInstance {
8+
id: string // Instance ID
9+
name: string // Server name (e.g., "Gmail", "GitHub")
10+
description: string | null // Server description
11+
tools: Array<{ name: string; description: string }> | null // Available tools
12+
authNeeded: boolean // Whether auth is required
13+
isAuthenticated: boolean // Whether currently authenticated
14+
serverUrl?: string // Server URL for this instance
15+
}
16+
17+
export interface CreateServerResponse {
18+
serverUrl: string // Full URL for connecting to the MCP server
19+
instanceId: string // Unique identifier for this server instance
20+
oauthUrl?: string | null // OAuth URL if authentication needed
21+
}
22+
23+
export interface ToolCallResult {
24+
success: boolean // Whether the call was successful
25+
result?: {
26+
content: any[] // Tool execution results
27+
isError?: boolean // Whether the result is an error
28+
}
29+
error?: string // Error message if failed
30+
}
31+
32+
export class KlavisAPIClient {
33+
private readonly baseUrl = 'https://api.klavis.ai'
34+
35+
constructor(private apiKey: string) {
36+
// Allow empty API key but operations will fail with clear error
37+
}
38+
39+
/**
40+
* Make HTTP request to Klavis API
41+
*/
42+
private async request<T>(
43+
method: string,
44+
path: string,
45+
body?: any,
46+
query?: Record<string, string>
47+
): Promise<T> {
48+
// Check for API key
49+
if (!this.apiKey) {
50+
throw new Error('Klavis API key not configured. Please add KLAVIS_API_KEY to your .env file.')
51+
}
52+
53+
let url = `${this.baseUrl}${path}`
54+
55+
// Add query parameters if provided
56+
if (query) {
57+
const params = new URLSearchParams(query)
58+
url += '?' + params.toString()
59+
}
60+
61+
const response = await fetch(url, {
62+
method,
63+
headers: {
64+
'Authorization': `Bearer ${this.apiKey}`,
65+
'Content-Type': 'application/json'
66+
},
67+
body: body ? JSON.stringify(body) : undefined
68+
})
69+
70+
if (!response.ok) {
71+
const errorText = await response.text()
72+
throw new Error(`Klavis API error: ${response.status} ${response.statusText} - ${errorText}`)
73+
}
74+
75+
return response.json()
76+
}
77+
78+
/**
79+
* Get all MCP server instances for a user
80+
* GET /user/instances
81+
*/
82+
async getUserInstances(userId: string, platformName: string): Promise<UserInstance[]> {
83+
const data = await this.request<{ instances: UserInstance[] }>(
84+
'GET',
85+
'/user/instances',
86+
undefined,
87+
{
88+
user_id: userId,
89+
platform_name: platformName
90+
}
91+
)
92+
93+
// Return instances directly without constructing serverUrl
94+
return data.instances || []
95+
}
96+
97+
/**
98+
* Create a new MCP server instance
99+
* POST /mcp-server/instance/create
100+
*/
101+
async createServerInstance(params: {
102+
serverName: string
103+
userId: string
104+
platformName: string
105+
}): Promise<CreateServerResponse> {
106+
return this.request<CreateServerResponse>(
107+
'POST',
108+
'/mcp-server/instance/create',
109+
{
110+
serverName: params.serverName,
111+
userId: params.userId,
112+
platformName: params.platformName,
113+
connectionType: 'StreamableHttp' // Always use StreamableHttp
114+
}
115+
)
116+
}
117+
118+
/**
119+
* List available tools for an MCP server
120+
* POST /mcp-server/list-tools
121+
*/
122+
async listTools(instanceId: string, serverSubdomain: string): Promise<any[]> {
123+
// Construct serverUrl from instanceId and serverSubdomain
124+
const serverUrl = `https://${serverSubdomain}-mcp-server.klavis.ai/mcp/?instance_id=${instanceId}`
125+
126+
const data = await this.request<{
127+
success: boolean
128+
tools?: any[]
129+
error?: string
130+
}>(
131+
'POST',
132+
'/mcp-server/list-tools',
133+
{
134+
serverUrl,
135+
format: 'openai', // Use native format for flexibility
136+
connectionType: 'StreamableHttp'
137+
}
138+
)
139+
140+
if (!data.success) {
141+
throw new Error(`Failed to list tools: ${data.error || 'Unknown error'}`)
142+
}
143+
144+
return data.tools || []
145+
}
146+
147+
/**
148+
* Call a tool on an MCP server
149+
* POST /mcp-server/call-tool
150+
*/
151+
async callTool(
152+
instanceId: string,
153+
serverSubdomain: string,
154+
toolName: string,
155+
toolArgs: any
156+
): Promise<ToolCallResult> {
157+
// Construct serverUrl from instanceId and serverSubdomain
158+
const serverUrl = `https://${serverSubdomain}-mcp-server.klavis.ai/mcp/?instance_id=${instanceId}`
159+
160+
try {
161+
const response = await this.request<ToolCallResult>(
162+
'POST',
163+
'/mcp-server/call-tool',
164+
{
165+
serverUrl,
166+
toolName,
167+
toolArgs: toolArgs || {},
168+
format: 'openai',
169+
connectionType: 'StreamableHttp'
170+
}
171+
)
172+
173+
return response
174+
} catch (error) {
175+
return {
176+
success: false,
177+
error: error instanceof Error ? error.message : 'Unknown error'
178+
}
179+
}
180+
}
181+
182+
/**
183+
* Delete a server instance
184+
* DELETE /mcp-server/instance/delete/{instance_id}
185+
*/
186+
async deleteServerInstance(instanceId: string): Promise<{ success: boolean; message?: string }> {
187+
return this.request<{ success: boolean; message?: string }>(
188+
'DELETE',
189+
`/mcp-server/instance/delete/${instanceId}`,
190+
undefined
191+
)
192+
}
193+
194+
/**
195+
* Get all available MCP servers
196+
* GET /mcp-server/servers
197+
*/
198+
async getAllServers(): Promise<Array<{
199+
id: string
200+
name: string
201+
description: string
202+
tools: Array<{ name: string; description: string }>
203+
authNeeded: boolean
204+
}>> {
205+
const data = await this.request<{ servers: any[] }>(
206+
'GET',
207+
'/mcp-server/servers',
208+
undefined
209+
)
210+
211+
return data.servers || []
212+
}
213+
214+
/**
215+
* Get authentication metadata for a server instance
216+
* GET /mcp-server/instance/get-auth/{instance_id}
217+
*/
218+
async getAuthMetadata(instanceId: string): Promise<{
219+
success: boolean
220+
authData?: any
221+
error?: string
222+
}> {
223+
try {
224+
return await this.request<{
225+
success: boolean
226+
authData?: any
227+
error?: string
228+
}>(
229+
'GET',
230+
`/mcp-server/instance/get-auth/${instanceId}`,
231+
undefined
232+
)
233+
} catch (error) {
234+
return {
235+
success: false,
236+
error: error instanceof Error ? error.message : 'Failed to get auth metadata'
237+
}
238+
}
239+
}
240+
241+
/**
242+
* Get instance status including authentication state
243+
* GET /mcp-server/instance/{instanceId}
244+
*/
245+
async getInstanceStatus(instanceId: string): Promise<{
246+
instanceId: string | null
247+
authNeeded: boolean
248+
isAuthenticated: boolean
249+
serverName: string
250+
platform: string
251+
externalUserId: string
252+
oauthUrl: string | null
253+
}> {
254+
return this.request<{
255+
instanceId: string | null
256+
authNeeded: boolean
257+
isAuthenticated: boolean
258+
serverName: string
259+
platform: string
260+
externalUserId: string
261+
oauthUrl: string | null
262+
}>(
263+
'GET',
264+
`/mcp-server/instance/${instanceId}`,
265+
undefined
266+
)
267+
}
268+
}

0 commit comments

Comments
 (0)