feat(core): track mcp prompt changes

This commit is contained in:
Aiden Cline 2026-06-29 18:47:57 -05:00
commit 4ef19f2e6a
5 changed files with 210 additions and 6 deletions

View file

@ -13,6 +13,8 @@ import {
ListToolsResultSchema,
type LoggingMessageNotification,
LoggingMessageNotificationSchema,
PromptListChangedNotificationSchema,
ResourceListChangedNotificationSchema,
ToolListChangedNotificationSchema,
ToolSchema,
} from "@modelcontextprotocol/sdk/types.js"
@ -79,7 +81,7 @@ export interface CallToolResult {
export interface LogMessage {
readonly level: LoggingMessageNotification["params"]["level"]
readonly logger: LoggingMessageNotification["params"]["logger"]
readonly logger?: LoggingMessageNotification["params"]["logger"]
readonly data: LoggingMessageNotification["params"]["data"]
}
@ -106,6 +108,10 @@ export interface Connection {
readonly onLog: (callback: (message: LogMessage) => void) => void
/** Registers a callback fired when the server announces its tool list changed; no-op if unsupported. */
readonly onToolsChanged: (callback: () => void) => void
/** Registers a callback fired when the server announces its prompt list changed; no-op if unsupported. */
readonly onPromptsChanged: (callback: () => void) => void
/** Registers a callback fired when the server announces its resource list changed; no-op if unsupported. */
readonly onResourcesChanged: (callback: () => void) => void
}
/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
@ -267,6 +273,14 @@ export const connect = Effect.fnUntraced(function* (
if (!client.getServerCapabilities()?.tools?.listChanged) return
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => callback())
},
onPromptsChanged: (callback) => {
if (!client.getServerCapabilities()?.prompts?.listChanged) return
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => callback())
},
onResourcesChanged: (callback) => {
if (!client.getServerCapabilities()?.resources?.listChanged) return
client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => callback())
},
} satisfies Connection
}

View file

@ -264,7 +264,16 @@ export const layer = Layer.effect(
entry.client = undefined
entry.tools = undefined
entry.status = { status: "failed", error: "Connection closed" }
fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore))
fork(
Effect.all(
[
events.publish(McpEvent.ToolsChanged, { server: name }),
events.publish(McpEvent.PromptsChanged, { server: name }),
events.publish(McpEvent.ResourcesChanged, { server: name }),
],
{ discard: true },
).pipe(Effect.ignore),
)
})
connection.onLog((message) => fork(serverLog(name, message).pipe(Effect.ignore)))
connection.onToolsChanged(() => {
@ -275,6 +284,12 @@ export const layer = Layer.effect(
),
)
})
connection.onPromptsChanged(() => {
fork(events.publish(McpEvent.PromptsChanged, { server: name }).pipe(Effect.ignore))
})
connection.onResourcesChanged(() => {
fork(events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore))
})
}
const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {

View file

@ -10,6 +10,20 @@ export const ToolsChanged = Event.define({
},
})
export const PromptsChanged = Event.define({
type: "mcp.prompts.changed",
schema: {
server: Schema.String,
},
})
export const ResourcesChanged = Event.define({
type: "mcp.resources.changed",
schema: {
server: Schema.String,
},
})
export const BrowserOpenFailed = Event.define({
type: "mcp.browser.open.failed",
schema: {
@ -18,4 +32,4 @@ export const BrowserOpenFailed = Event.define({
},
})
export const Definitions = Event.inventory(ToolsChanged, BrowserOpenFailed)
export const Definitions = Event.inventory(ToolsChanged, PromptsChanged, ResourcesChanged, BrowserOpenFailed)

View file

@ -279,6 +279,8 @@ import type {
V2FsListResponses,
V2FsReadErrors,
V2FsReadResponses,
V2GenerateTextErrors,
V2GenerateTextResponses,
V2HealthGetErrors,
V2HealthGetResponses,
V2IntegrationAttemptCancelErrors,
@ -5944,6 +5946,48 @@ export class Model extends HeyApiClient {
}
}
export class Generate extends HeyApiClient {
/**
* Generate text
*
* Run one stateless model generation at the requested location and return the assistant text. Uses the location's default model when none is specified.
*/
public text<ThrowOnError extends boolean = false>(
parameters?: {
location?: {
directory?: string
workspace?: string
}
prompt?: string
model?: ModelRef
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "location" },
{ in: "body", key: "prompt" },
{ in: "body", key: "model" },
],
},
],
)
return (options?.client ?? this.client).post<V2GenerateTextResponses, V2GenerateTextErrors, ThrowOnError>({
url: "/api/generate",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}
export class Provider2 extends HeyApiClient {
/**
* List providers
@ -7233,6 +7277,11 @@ export class V2 extends HeyApiClient {
return (this._model ??= new Model({ client: this.client }))
}
private _generate?: Generate
get generate(): Generate {
return (this._generate ??= new Generate({ client: this.client }))
}
private _provider?: Provider2
get provider(): Provider2 {
return (this._provider ??= new Provider2({ client: this.client }))

View file

@ -80,6 +80,8 @@ export type Event =
| EventTuiToastShow2
| EventTuiSessionSelect2
| EventMcpToolsChanged
| EventMcpPromptsChanged
| EventMcpResourcesChanged
| EventMcpBrowserOpenFailed
| EventCommandExecuted
| EventProjectUpdated
@ -646,7 +648,6 @@ export type Prompt = {
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
system?: string
}
export type Pty = {
@ -1513,6 +1514,20 @@ export type GlobalEvent = {
server: string
}
}
| {
id: string
type: "mcp.prompts.changed"
properties: {
server: string
}
}
| {
id: string
type: "mcp.resources.changed"
properties: {
server: string
}
}
| {
id: string
type: "mcp.browser.open.failed"
@ -2760,7 +2775,6 @@ export type PromptInput = {
text: string
files?: Array<PromptInputFileAttachment>
agents?: Array<PromptAgentAttachment>
system?: string
}
export type ConflictError = {
@ -2834,6 +2848,12 @@ export type SessionMessagesResponse = {
}
}
export type GenerateTextResponse = {
data: {
text: string
}
}
export type ProviderNotFoundError = {
_tag: "ProviderNotFoundError"
providerID: string
@ -2999,6 +3019,8 @@ export type V2Event =
| TuiToastShow
| TuiSessionSelect
| McpToolsChanged
| McpPromptsChanged
| McpResourcesChanged
| McpBrowserOpenFailed
| CommandExecuted
| ProjectUpdated
@ -4096,7 +4118,6 @@ export type SessionMessageUser = {
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
system?: string
type: "user"
}
@ -6036,6 +6057,40 @@ export type McpToolsChanged = {
}
}
export type McpPromptsChanged = {
id: string
metadata?: {
[key: string]: unknown
}
type: "mcp.prompts.changed"
durable?: {
aggregateID: string
seq: number
version: number
}
location?: LocationRef
data: {
server: string
}
}
export type McpResourcesChanged = {
id: string
metadata?: {
[key: string]: unknown
}
type: "mcp.resources.changed"
durable?: {
aggregateID: string
seq: number
version: number
}
location?: LocationRef
data: {
server: string
}
}
export type McpBrowserOpenFailed = {
id: string
metadata?: {
@ -7117,6 +7172,22 @@ export type EventMcpToolsChanged = {
}
}
export type EventMcpPromptsChanged = {
id: string
type: "mcp.prompts.changed"
properties: {
server: string
}
}
export type EventMcpResourcesChanged = {
id: string
type: "mcp.resources.changed"
properties: {
server: string
}
}
export type EventMcpBrowserOpenFailed = {
id: string
type: "mcp.browser.open.failed"
@ -12353,6 +12424,47 @@ export type V2ModelListResponses = {
export type V2ModelListResponse = V2ModelListResponses[keyof V2ModelListResponses]
export type V2GenerateTextData = {
body: {
prompt: string
model?: ModelRef
}
path?: never
query?: {
location?: {
directory?: string
workspace?: string
}
}
url: "/api/generate"
}
export type V2GenerateTextErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* ServiceUnavailableError
*/
503: ServiceUnavailableError
}
export type V2GenerateTextError = V2GenerateTextErrors[keyof V2GenerateTextErrors]
export type V2GenerateTextResponses = {
/**
* GenerateTextResponse
*/
200: GenerateTextResponse
}
export type V2GenerateTextResponse = V2GenerateTextResponses[keyof V2GenerateTextResponses]
export type V2ProviderListData = {
body?: never
path?: never