内容
Open AI
MCP Open AI 是一个用于连接实时客户端到 OpenAI 实时会话并调度实时函数调用到服务器端 Kotlin 工具的 Kotlin/Spring Boot 包。
使用,您需要:
- 从后端创建短期的 OpenAI 实时客户端密钥。
-实时调用打开服务器端辅助 WebSocket。
- 注册 OpenAI 可以在对话期间调用的 Kotlin 工具。
- 将工具结果返回到同一个实时对话中。
- 在您的应用程序中监听实时生命周期和对话事件。
##
- Java 23
- Kotlin 2.3+
- Spring Boot 4.- Spring WebFlux 具有实时 API 访问 OpenAI API 密钥
## 安装
将包添加到您的应用程序中。
```kotlin
dependencies {
implementation("io.githuback:mcp-openai.0.0")
}
Spring Boot 设置
确保 Spring 扫描包组件。该包从其自己的配置中注册 `McpProperties`。
```kotlin
package com.example.demo
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication(
scanBasePackages = [
"com.example.demo",
"io.github.kostack.mcp_open_ai"
]
)
class DemoApplication
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}
```
如果您的应用程序没有现有的 `WebClient` Bean,请注册一个。
```kotlin
package com.example.demo
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.client.WebClient
@Configuration
class HttpConfiguration {
@Bean
fun webClient(): WebClient = WebClient.create()
}
```
## 配置
使用 `kostack-mcp` 前缀配置包。
```yaml
kostack-mcp:
api-key: ${OPENAI_API_KEY}
model: gpt-realtime-mini transcription-model: gpt-realtime-whisper
enable-audio: false
```
可用属性:
| 属性 | 默认值 | 描述 |
| --- | --- | --- |
| `api-key` | 空 | 您的服务器端 OpenAI API 密钥。不要将其暴露给浏览器。 |
| `client-secrets-url` | `https://api.openai.com/v1/realtime/client_secrets` | 用于创建临时实时客户端密钥的 OpenAI 端点。 |
|band-url` | `wss://api.openai.com/v1/realtime` | OpenAI 辅助 WebSocket 端点。 |
| `sideband-prefix` | `/api/realtime` | 构建的实时由的前缀。 |
model` | `gpt-mini` | 创建会话时使用的实时模型。 |
transcription-model` | `gpt-realtime-whisper` | 用于输入音频的音频模型。 |
| `udio` | `false` | 应用程序级标志,您可以在决定是否允许音频请求。 |
## 核心概念
### 命名空间空间对工具和路由。当为命名空间创建令牌时,只有该命名空间的工具会被发送到 OpenAI。当工具调用返回时,分发器会执行具有匹配命名空间和名称的工具。
在单个应用程序具有单独的助手、租户或功能区域时使用命名空间。
### 通道
通道在 `ConversationStore` 中标识对话历史记录。为对话使用稳定的值 ID、票 ID 或。
### 调用 ID
创建实时 WebRTC 会话时从 OpenAI 接收调用 ID。将该调用 ID 发送到您的后端,以便包可以打开匹配的服务器端辅助连接。
## 定义工具
为工具参数创建请求数据类。
```kotlin
package com.example.demo.tools
import kotlinx.serialization.Serializable
import org.springframework.ai.toolParam
@Serializable
data class WeatherRequest(
@ToolParam(description = "城市雅典或柏林")
val city: String
)
```
创建结果类型。
```kotlin
package com.example.demodata class WeatherResult val city: String,
val summary: String,
val temperatureCelsius: Int
)
```
实现一个扩展Tool` 的 Spring 组件。
```kotlin
package com.example.demo.tools
import io.github.kostack.mcp_open.ToolContext
import io.github.kostack.mcp_open.ToolDefinition
import io.github.mcp.dto.ToolResult
import io.github.kostack.mcp_open_ai.tool.AbstractTool
.github.kostack_open_ai.utils.ToolSchemaUtils
import org.springframework.stereotype.Component
@Component
class WeatherTool : AbstractTool() {
override val namespace: String = "support_assistant"
override val toolName: String = "get_weather"
override val description: String = "获取城市天气。"
override fun getDefinition(): ToolDefinition =
ToolDefinition(
namespace = namespace,
name = toolName,
description = description = ToolSchemaUtils.toParameters<WeatherRequest>()
)
override suspend fun execute ToolContext): ToolResult {
val request = context<WeatherRequest>()
val result =
WeatherResult(
city = request.city summary = "晴 temperatureCelsius = 27
)
return(success = true, result = result)
}
}
OpenAI 调用 `` 时,包会将 JSON 参数解码为 `WeatherRequest`,运行 `execute`,并将 `ToolResult` 发送回 OpenAI。
## 后端路由
包提供了一个默认的 `SidebandConfigurationRouter`,当 Spring 扫描 `io.github.kostack.mcp_open_ai` 时。该默认路由使用 `kostack-mcp.sideband-prefix`,默认为 `/api`:
- `POST/realtime/token`
- `POST /api/realtime/connect`
- `POST/realtime/disconnect`
请求体携带 `namespace`、`channel` 和语言/音频设置。处理程序使用 `request.namespace` 选择工具定义令牌指令故意受到限制;在 `RealtimeEvents_PRE_CREATE` 监听器中设置应用程序指令。
### 创建令牌
请求:
```http
POST /api/realtime/token
Content application/json
{
": "support_assistant",
"channel": "conversation-123",
"language": "zh",
"audioEnabled": false
}
```
响应:
```json
{
"clientSecret": "ek_..."
}
```
仅在请求它的浏览器会话中使用返回的 `clientSecret`。
### 连接辅助通道
在浏览器创建实时 WebRTC 调用后,将 OpenAI 调用 ID 和客户端密钥发送到您的后端。
```http
POST/realtime/connect
Content-Type: application/json
{
"callId": "call_abc123",
"clientSecret": "ek_ "namespace": "support_assistant",
"channel": "conversation-123",
"language": "zh",
"audioEnabled": false
}
```
成功响应:
```http
204 No Content
```
### 断开辅助通道
```http
POST /api/realtime/disconnect
Content-Type: application/json
{
"callId": "call_abc123",
"namespace": "support_assistant",
"channel": "conversation-123"
}
```
成功响应:
```http
204 No Content
```
## 浏览器流程示例
浏览器负责使用 连接到 实时服务。后端负责创建临时客户端密钥连接。
```javascript channel = "conversation-const namespace = "support_assistant";
const language = "zh";
async function createToken() {
const response("/api/re", {
methodPOST",
headers "Content-Type": "" },
body: JSON.stringify({
namespace,
channel,
language,
audioEnabled: false
})
});
return response.json();
}
function extractCallId(locationHeader) {
const match = locationHeader.match(/\/v1\/realtime\/calls\/([^/?#]+)/);
if (!match) throw new Error("Could not read Realtime call return match[1];
}
async function connectRealtime() {
const { clientSecret } = await createToken();
const pc = new RTCPeerConnection();
const dc = pc.createDataChannelai-events");
message = (event {
const realtimeEvent.parse(event.data);
console.log("实时事件", realtimeEvent);
};
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const sdpResponse = await fetch("https://api.openai.com/v1/realtime/calls", {
method: "POST",
headers: {
"Authorization": `Bearer ${clientSecret}`,
"Content-Type": "application/sdp"
},
body: offer.sdp
const answer = await sdpResponse.text();
await pc.setRemoteDescription({ type: "answer", sdp: answer });
const callId = extractCallId(sdpResponse.headers.get("Location"));
await fetch("/api/realtime/connect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
callId,
clientSecret,
namespace,
channel,
language,
audioEnabled: false
})
});
return { pc, dc, callId };
}
```
通过实时数据通道发送文本消息:
```javascript
function sendText(dc, text) {
dc.send(JSON.stringify({
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_text",
text
}
]
}
}));
dc.send(JSON.stringify({
type: "response.create",
response: {
output_modalities: ["text"]
}
}));
}
```
当会话结束时断开连接:
```javascript
async function disconnectRealtime(pc, callId) {
await fetch("/api/realtime/disconnect", {
method: "POST",
headers "Content-Type":/json" },
body: JSON.stringify({ callId, namespace, channel })
});
pc.close();
}
监听事件
`io.github.k:event-dispatcher` 发布 `@SuspendListener暂停监听器。
package com.exampleimport io.github.kostack.event_dispatcher.SuspendListener
import io.github.kostack.mcp_open_ai.RealtimeEvents
import io.github.kostack.mcp_open_ai.event.RealtimeConnectEvent
import io.github.kostack.mcp_open_ai.event.RealtimeHandlerEvent
import io.github.kostack.mcp_open_ai.event.RealtimeTokenPreCreateEvent
import org.springframework.stereotype.Component
@Component
class RealtimeListeners {
@SuspendListener(RealtimeEvents.TOKEN_PRE_CREATE)
suspend fun onTokenPreCreate(event: RealtimeTokenPreCreateEvent) {
event.instructions =
"""
您是一个有帮助的支持助手。
请以用户选择的语言回复。
会话历史:
:history_items
""".trimIndent()
}
@SuspendListener(RealtimeEvents.CONNECT)
suspend fun onConnect(event: RealtimeConnectEvent) {
println("已连接调用 ${event.request.callId}")
}
@SuspendListener(RealtimeEvents.SESSION_START)
suspend fun onSessionStart(event: RealtimeConnectEvent) {
println("辅助会话已启动,调用 ID 为 ${event.request.callId}")
}
SuspendListener(RealtimeEvents.RESPONSE_DONE)
onResponseDone(eventaltimeHandlerEvent) {
println("响应已完成,通道为 ${event.request.channel}")
}
}
```
已发布的事件名称:
| 事件 | 发布时 |
| --- | --- |
| `kostack_mcp.token.pre_create` | 在创建临时令牌之前。 |
| `kostack_mcp.connect` | 在启动辅助连接之前。 |
| `kostack_mcp.disconnect` | 在取消辅助连接之前。 |
| `kostack_mcp.session.start` | 在辅助 WebSocket 会话可用后。 |
| `kostack_mcp.conversation.item.done` | 当收到完整的对话项时。 `kostack_mcp.conversation.item.input_audio.completed` | 当输入录完成时| `kostack.response.output.item.done 当响应输出项。 |
|ack_mcp.response.done` | 当响应完成时。 |
| `kostack_mcp.response.error` OpenAI 发送错误事件时。 |
| `_mcp.session.updated 当实时会话更新## 会话`ConversationStore` 在内存中按通道存储对话历史记录。包含的 `ConversationListener` 记录用户和助手消息,并在创建令牌时替换 `RealtimeTokenPreCreateEvent.instructions` 中的 `:history_items`。
示例指令:
```text
您是一个支持助手。
使用此会话历史:
:history_items
```
对于生产应用程序,请视为内存中的默认值。如果您需要跨重启的持久历史记录,请持久化历史记录并牌预创建监听器中更新 `RealtimeTokenPreCreateEvent.instructions`。
## 音频
仅当您的应用程序支持麦克风访问和音频输出时,请在令牌和辅助请求中将 `audioEnabled` 设置为 `true`。如果配置中的 `enable-audio` 为 `false`,则处理请求音频关闭。
启用音频时,会话创建
- 服务器 V换行检测 使用 `kostack-mcp.transcription-model` 的输入转录。
- 输出语音设置为 `alloy`。
## 错误处理
执行抛出异常,Dispatcher` 返回:
```json
{
success": false,
result": "执行工具:tool_name:..."
}
```
对于预期的业务失败,请返回正常的 `ToolResult`:
```kotlin
return ToolResult(
success = false,
result = mapOf("message" to "未找到匹配的账户")
)
```
## 直接测试工具
无需打开实时会话即可测试工具。
```kotlin
package com.example.demo.tools
import io.github.kostack.mcp_open_ai.dto.ToolContext
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertTrue
class WeatherToolTest {
@Test
fun `返回天气`() = runTest {
val tool = WeatherTool()
val result =
tool.execute(
ToolContext(
namespace = "support_assistant",
channel = "test-channel",
rawRequest = """{"city":"Athens"}"""
)
assert.success)
}
## 完整的示例
```kotlin
package com.example.demo
.github.kost_dispatcher.SuspendListener
import io.github.kostack_open_ai.Re
import io.githuback.mcp.event.RealtimeTokenEvent
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.stereotype.Component
@SpringBootApplication(
scanBasePackages = [
"com.example.demo",
"io.github.kostack.mcp_open_ai"
]
)
class DemoApplication
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}
@Configuration
class DemoConfiguration {
@Bean
fun webClient(): WebClient = WebClient.create()
}
@Component
class DemoRealtimeListeners {
@SuspendListener(RealtimeEvents.TOKEN_PRE_CREATE)
suspend fun onTokenPreCreate(event: RealtimeTokenPreCreateEvent) {
event.instructions = "您是一个有帮助的助手。请用英语回复。"
}
}
```
## 安全注意事项
- 保持 `kostack-mcp.api-key` 在服务器端。
- 仅向浏览器发送临时的实时客户端密钥。
- 使用您的应用程序身份验证保护令牌和辅助端点。
- 验证当前用户是否有权访问请求的通道。
- 不要仅因为参数来自模型就信任工具参数;在工具实现中验证它们。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器