Skip to content

Commit 06ffe9d

Browse files
samuvliamstorkey-elmo
authored andcommitted
feat(feature-flag): playground experimental chat (stacklok#741)
* feat: initial playground * refactor: complete refactor and overhaul * enable pre-release * fix: adjust unused var, code and deps * feat: use tenstack query for feature flags and put playground around it * revert: remove prerelease true
1 parent fa7a720 commit 06ffe9d

35 files changed

+5914
-12
lines changed

main/src/chat/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Export all chat-related functionality
2+
export * from './types'
3+
export * from './providers'
4+
export * from './storage'
5+
export * from './mcp-tools'
6+
export * from './streaming'
7+
export * from './stream-utils'

main/src/chat/mcp-tools.ts

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import {
2+
experimental_createMCPClient as createMCPClient,
3+
type experimental_MCPClient as MCPClient,
4+
type experimental_MCPClientConfig as MCPClientConfig,
5+
} from 'ai'
6+
import type { ToolSet } from 'ai'
7+
import { Experimental_StdioMCPTransport as StdioMCPTransport } from 'ai/mcp-stdio'
8+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
9+
import { createClient } from '@api/client'
10+
import { getApiV1BetaWorkloads } from '@api/sdk.gen'
11+
import type { CoreWorkload } from '@api/types.gen'
12+
import { getHeaders } from '../headers'
13+
import { getToolhivePort } from '../toolhive-manager'
14+
import log from '../logger'
15+
import type { McpToolInfo } from './types'
16+
import { getEnabledMcpTools } from './storage'
17+
18+
// Interface for MCP tool definition from client
19+
interface McpToolDefinition {
20+
description?: string
21+
inputSchema?: {
22+
properties?: Record<string, unknown>
23+
}
24+
}
25+
26+
// Type guard to check if an object is a valid MCP tool definition
27+
function isMcpToolDefinition(obj: unknown): obj is McpToolDefinition {
28+
if (!obj || typeof obj !== 'object') return false
29+
30+
const tool = obj as Record<string, unknown>
31+
32+
// Description should be string if present
33+
if ('description' in tool && typeof tool.description !== 'string')
34+
return false
35+
36+
// InputSchema should be object if present
37+
if ('inputSchema' in tool) {
38+
if (!tool.inputSchema || typeof tool.inputSchema !== 'object') return false
39+
40+
const inputSchema = tool.inputSchema as Record<string, unknown>
41+
if (
42+
'properties' in inputSchema &&
43+
inputSchema.properties !== null &&
44+
typeof inputSchema.properties !== 'object'
45+
) {
46+
return false
47+
}
48+
}
49+
50+
return true
51+
}
52+
53+
// Create transport configuration based on workload type
54+
function createTransport(
55+
workload: CoreWorkload,
56+
serverName: string,
57+
port: number
58+
): MCPClientConfig {
59+
const transportConfigs = {
60+
stdio: () => ({
61+
name: serverName,
62+
transport: new StdioMCPTransport({
63+
command: 'node',
64+
args: [],
65+
}),
66+
}),
67+
'streamable-http': () => {
68+
const url = new URL(workload.url || `http://localhost:${port}/mcp`)
69+
return {
70+
name: serverName,
71+
transport: new StreamableHTTPClientTransport(url),
72+
}
73+
},
74+
sse: () => ({
75+
name: serverName,
76+
transport: {
77+
type: 'sse' as const,
78+
url: `${workload.url || `http://localhost:${port}/sse#${serverName}`}`,
79+
},
80+
}),
81+
default: () => ({
82+
name: serverName,
83+
transport: {
84+
type: 'sse' as const,
85+
url: `${workload.url || `http://localhost:${port}/sse#${serverName}`}`,
86+
},
87+
}),
88+
}
89+
90+
// Check if transport_type is stdio but URL suggests SSE
91+
let transportType = workload.transport_type as keyof typeof transportConfigs
92+
93+
if (transportType === 'stdio' && workload.url) {
94+
// If URL contains /sse or #, use SSE transport instead
95+
if (workload.url.includes('/sse') || workload.url.includes('#')) {
96+
// Override stdio to SSE based on URL pattern
97+
transportType = 'sse'
98+
}
99+
}
100+
101+
const configBuilder =
102+
transportConfigs[transportType] || transportConfigs.default
103+
return configBuilder()
104+
}
105+
106+
// Get MCP server tools information
107+
export async function getMcpServerTools(serverName?: string): Promise<
108+
| McpToolInfo[]
109+
| {
110+
serverName: string
111+
serverPackage?: string
112+
tools: Array<{
113+
name: string
114+
description?: string
115+
parameters?: Record<string, unknown>
116+
enabled: boolean
117+
}>
118+
isRunning: boolean
119+
}
120+
| null
121+
> {
122+
try {
123+
const port = getToolhivePort()
124+
const client = createClient({
125+
baseUrl: `http://localhost:${port}`,
126+
headers: getHeaders(),
127+
})
128+
129+
const { data } = await getApiV1BetaWorkloads({
130+
client,
131+
})
132+
const workloads = data?.workloads
133+
134+
// If serverName is provided, return server-specific format
135+
if (serverName) {
136+
// Get server tools for specific server
137+
138+
const workload = (workloads || []).find(
139+
(w) => w.name === serverName && w.tool_type === 'mcp'
140+
)
141+
142+
if (!workload) {
143+
return null
144+
}
145+
146+
// Get enabled tools for this server
147+
const enabledTools = getEnabledMcpTools()
148+
const enabledToolNames = enabledTools[serverName] || []
149+
150+
// If workload.tools is empty, try to discover tools by connecting to the server
151+
let discoveredTools: string[] = workload.tools || []
152+
const serverMcpTools: Record<string, McpToolDefinition> = {}
153+
154+
if (discoveredTools.length === 0 && workload.status === 'running') {
155+
try {
156+
// Try to create an MCP client and discover tools
157+
const config = createTransport(workload, serverName, port!)
158+
if (config) {
159+
const mcpClient = await createMCPClient(config)
160+
const rawTools = await mcpClient.tools()
161+
162+
// Filter and validate tools using type guard
163+
for (const [toolName, toolDef] of Object.entries(rawTools)) {
164+
if (isMcpToolDefinition(toolDef)) {
165+
serverMcpTools[toolName] = toolDef
166+
}
167+
}
168+
169+
discoveredTools = Object.keys(serverMcpTools)
170+
await mcpClient.close()
171+
}
172+
} catch (error) {
173+
log.error(`Failed to discover tools for ${serverName}:`, error)
174+
}
175+
}
176+
177+
const result = {
178+
serverName: workload.name!,
179+
serverPackage: workload.package,
180+
tools: discoveredTools.map((toolName) => {
181+
const toolDef = serverMcpTools[toolName]
182+
return {
183+
name: toolName,
184+
description: toolDef?.description || '',
185+
parameters: toolDef?.inputSchema?.properties || {},
186+
enabled: enabledToolNames.includes(toolName),
187+
}
188+
}),
189+
isRunning: workload.status === 'running',
190+
}
191+
192+
return result
193+
}
194+
195+
// Otherwise return the original format for backward compatibility
196+
const mcpTools = (workloads || [])
197+
.filter(
198+
(workload) =>
199+
workload.name && workload.tools && workload.tool_type === 'mcp'
200+
)
201+
.flatMap((workload) =>
202+
workload.tools!.map((toolName) => ({
203+
name: `mcp_${workload.name}_${toolName}`,
204+
description: '',
205+
inputSchema: {},
206+
serverName: workload.name!,
207+
}))
208+
)
209+
210+
return mcpTools
211+
} catch (error) {
212+
log.error('Failed to get MCP server tools:', error)
213+
return serverName ? null : []
214+
}
215+
}
216+
217+
// Create MCP tools for AI SDK
218+
export async function createMcpTools(): Promise<{
219+
tools: ToolSet
220+
clients: MCPClient[]
221+
}> {
222+
const mcpTools: ToolSet = {}
223+
const mcpClients: MCPClient[] = []
224+
225+
try {
226+
const port = getToolhivePort()
227+
const client = createClient({
228+
baseUrl: `http://localhost:${port}`,
229+
headers: getHeaders(),
230+
})
231+
232+
const { data } = await getApiV1BetaWorkloads({
233+
client,
234+
})
235+
const workloads = data?.workloads
236+
237+
// Get enabled tools from storage
238+
const enabledTools = getEnabledMcpTools()
239+
240+
if (Object.keys(enabledTools).length === 0) {
241+
return { tools: mcpTools, clients: mcpClients }
242+
}
243+
244+
// Create MCP clients for each server with enabled tools
245+
for (const [serverName, toolNames] of Object.entries(enabledTools)) {
246+
if (toolNames.length === 0) continue
247+
248+
const workload = workloads?.find((w) => w.name === serverName)
249+
if (!workload || workload.tool_type !== 'mcp') continue
250+
251+
try {
252+
const config = createTransport(workload, serverName, port!)
253+
254+
const mcpClient = await createMCPClient(config)
255+
256+
mcpClients.push(mcpClient)
257+
258+
// Get all tools from the MCP server using schema discovery
259+
const serverMcpTools = await mcpClient.tools()
260+
261+
// Add only the enabled tools from this server
262+
for (const toolName of toolNames) {
263+
if (serverMcpTools[toolName]) {
264+
mcpTools[toolName] = serverMcpTools[toolName]
265+
}
266+
}
267+
268+
// MCP client created successfully
269+
} catch (error) {
270+
log.error(`Failed to create MCP client for ${serverName}:`, error)
271+
}
272+
}
273+
274+
// MCP tools created
275+
} catch (error) {
276+
log.error('Failed to create MCP tools:', error)
277+
}
278+
279+
return { tools: mcpTools, clients: mcpClients }
280+
}

0 commit comments

Comments
 (0)