Skip to content
This repository was archived by the owner on Jul 29, 2025. It is now read-only.

Commit 81d3c15

Browse files
committed
docs(client/tools): add xsai example
1 parent f401292 commit 81d3c15

File tree

4 files changed

+181
-0
lines changed

4 files changed

+181
-0
lines changed

docs/content/docs/client/shared/tools.mdx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,94 @@ await client.callTool('add', { a: 1, b: 1 })
3838
await client.callTool('echo', { message: 'Hello, World!' })
3939
await client.callTool('getTinyImage', {})
4040
```
41+
42+
## integration with [xsAI](https://xsai.js.org) (experimental)
43+
44+
> This feature is yet to be tested and feedback is welcome.
45+
46+
### Library
47+
48+
```ts
49+
import type { AudioPart, ImagePart, TextPart, Tool as XSAITool } from '@xsai/shared-chat'
50+
import type { Client } from '@xsmcp/client-shared'
51+
import type { CallToolResult } from '@xsmcp/shared'
52+
53+
import { rawTool } from '@xsai/tool'
54+
55+
const toXSAIContent = (contents: CallToolResult['content']): (AudioPart | ImagePart | TextPart)[] =>
56+
// eslint-disable-next-line array-callback-return
57+
contents.map((content) => {
58+
switch (content.type) {
59+
case 'audio':
60+
return {
61+
input_audio: {
62+
data: content.data,
63+
format: content.mimeType === 'audio/wav'
64+
? 'wav'
65+
// TODO: fallback
66+
: 'mp3',
67+
},
68+
type: 'input_audio',
69+
} satisfies AudioPart
70+
case 'image':
71+
return {
72+
image_url: { url: content.data },
73+
type: 'image_url',
74+
} satisfies ImagePart
75+
case 'resource':
76+
return {
77+
text: ('text' in content.resource
78+
? content.resource.text
79+
// TODO: fallback
80+
: content.resource.blob
81+
),
82+
type: 'text',
83+
} satisfies TextPart
84+
case 'text':
85+
return {
86+
text: content.text,
87+
type: 'text',
88+
} satisfies TextPart
89+
}
90+
})
91+
92+
export const getTools = async (client: Client): Promise<XSAITool[]> =>
93+
client
94+
.listTools()
95+
.then(({ tools }) => tools.map(tool => rawTool({
96+
description: tool.description,
97+
execute: async params => client.callTool(tool.name, params as Record<string, unknown>)
98+
// eslint-disable-next-line sonarjs/no-nested-functions
99+
.then(result => toXSAIContent(result.content)),
100+
name: tool.name,
101+
parameters: tool.inputSchema,
102+
})))
103+
```
104+
105+
### Usage
106+
107+
```ts
108+
import { createHttpTransport } from '@xsmcp/client-http'
109+
import { createClient } from '@xsmcp/server-shared'
110+
111+
import { getTools } from './get-tools'
112+
113+
const client = createClient({
114+
name: 'example-client',
115+
transport: createHttpTransport({ url: 'http://localhost:3000/mcp' }),
116+
version: '1.0.0',
117+
})
118+
119+
const mcpTools = await client.listTools()
120+
121+
const result = await streamText({
122+
baseURL: 'http://localhost:11434/v1/',
123+
messages: [{
124+
content: 'What is the weather in Brooklyn, New York?',
125+
role: 'user',
126+
}],
127+
model: 'gemma3',
128+
onFinish: async () => await client.close(),
129+
tools: await getTools(client),
130+
})
131+
```

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
"@importantimport/tsconfig": "^1.0.0-beta.2",
2020
"@pnpm/find-workspace-dir": "^1000.1.0",
2121
"@types/node": "^22.15.24",
22+
"@xsai/shared-chat": "0.3.0-beta.3",
23+
"@xsai/tool": "0.3.0-beta.3",
2224
"automd": "^0.4.0",
2325
"bumpp": "^10.1.1",
2426
"eslint": "^9.27.0",
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { AudioPart, ImagePart, TextPart, Tool as XSAITool } from '@xsai/shared-chat'
2+
import type { Client } from '@xsmcp/client-shared'
3+
import type { CallToolResult } from '@xsmcp/shared'
4+
5+
import { rawTool } from '@xsai/tool'
6+
7+
const toXSAIContent = (contents: CallToolResult['content']): (AudioPart | ImagePart | TextPart)[] =>
8+
// eslint-disable-next-line array-callback-return
9+
contents.map((content) => {
10+
switch (content.type) {
11+
case 'audio':
12+
return {
13+
input_audio: {
14+
data: content.data,
15+
format: content.mimeType === 'audio/wav'
16+
? 'wav'
17+
// TODO: fallback
18+
: 'mp3',
19+
},
20+
type: 'input_audio',
21+
} satisfies AudioPart
22+
case 'image':
23+
return {
24+
image_url: { url: content.data },
25+
type: 'image_url',
26+
} satisfies ImagePart
27+
case 'resource':
28+
return {
29+
text: ('text' in content.resource
30+
? content.resource.text
31+
// TODO: fallback
32+
: content.resource.blob
33+
),
34+
type: 'text',
35+
} satisfies TextPart
36+
case 'text':
37+
return {
38+
text: content.text,
39+
type: 'text',
40+
} satisfies TextPart
41+
}
42+
})
43+
44+
export const getTools = async (client: Client): Promise<XSAITool[]> =>
45+
client
46+
.listTools()
47+
.then(({ tools }) => tools.map(tool => rawTool({
48+
description: tool.description,
49+
execute: async params => client.callTool(tool.name, params as Record<string, unknown>)
50+
// eslint-disable-next-line sonarjs/no-nested-functions
51+
.then(result => toXSAIContent(result.content)),
52+
name: tool.name,
53+
parameters: tool.inputSchema,
54+
})))

pnpm-lock.yaml

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)