1
/**2
* Scoped MCP resource providers and the shared model-facing resource tools.3
*4
* @module @deepseek-ai/dsh-mcp-resources5
*/7
import { Service, type Context } from '@deepseek-ai/cordis'8
import { createScope, NamedEntries, ScopedLayers, scopeOf, type ScopeKey, type ScopeLayer } from '@deepseek-ai/dsh-scope'9
import type { JsonValue } from '@deepseek-ai/dsh-util-values'10
import type { ToolExecution } from '@deepseek-ai/dsh-tools'11
import type {} from '@deepseek-ai/dsh-system-prompt'12
import { registerResourceTools } from './tools.ts'14
declare module '@deepseek-ai/cordis' {15
interface Context {16
mcpResources: McpResourceRuntime17
}18
}20
/** One supported resource operation, with server-owned cursors and URIs. */21
export type McpResourceRequest =22
| { method: 'resources/list' | 'resources/templates/list'; cursor?: string }23
| { method: 'resources/read'; uri: string }25
/** One configured server's resource access, owned by its MCP connection plugin. */26
export interface McpResourceProvider {27
/**28
* Run an operation against one live connection generation.29
* @param request - MCP resource method and parameters.30
* @param exec - caller identity and cancellation for this invocation.31
* @returns the protocol result as lossless JSON.32
*/33
request(request: McpResourceRequest, exec: ToolExecution): Promise<JsonValue>34
}36
class ResourceLayer implements ScopeLayer {37
readonly servers = new NamedEntries<McpResourceProvider>(name =>38
new Error(`MCP resource server "${name}" is already registered in this scope`))39
disposeTools: (() => void | Promise<void>) | undefined41
isEmpty(): boolean {42
return this.servers.isEmpty()43
}44
}46
/** Scoped resource access plus three tools shared by configured MCP servers. */47
export class McpResourceRuntime extends Service {48
/** Tool registry required by the resource consumer. */49
static inject = ['tools']51
private readonly layers = new ScopedLayers(() => new ResourceLayer(), () => undefined)52
/** Shared tool registrations outlive any one server's registering context. */53
private readonly selfCtx: Context55
constructor(ctx: Context) {56
super(ctx, 'mcpResources')57
this.selfCtx = ctx59
ctx.inject(['systemPrompt'], (inner) => {60
inner.systemPrompt.section({61
name: 'mcp-resource-servers',62
order: inner.systemPrompt.getSectionOrder('MCP_SERVERS'),63
interpolate: false,64
text: ({ scope }) => {65
const names = [...this.layers.merge(scope, layer => layer.servers).keys()].sort()66
return names.length === 0 ? '' : '## MCP resource servers\n\n'67
+ 'Use list_mcp_resources, list_mcp_resource_templates, or read_mcp_resource with one of these names '68
+ `as the server argument: ${JSON.stringify(names)}.`69
},70
})71
})72
}74
/**75
* Register one server and expose resource tools while that scope has providers.76
* @param server - configured server name, unique in this scope.77
* @param provider - connection-owned resource operations.78
* @returns the effect disposer for this exact registration.79
*/80
register(server: string, provider: McpResourceProvider): () => void {81
const ctx = this.ctx82
const scope = scopeOf(ctx)83
const dispose = ctx.effect(function* (this: McpResourceRuntime) {84
let disposal: void | Promise<void>85
// Tools disappear synchronously; Cordis owns any pending scoped-fiber teardown.86
yield () => disposal87
yield this.layers.effect(ctx, (layer) => {88
const first = layer.servers.isEmpty()89
const remove = layer.servers.insert(server, provider)90
try {91
if (first) layer.disposeTools = this.registerTools(scope)92
} catch (error) {93
remove()94
throw error95
}96
return () => {97
remove()98
// oxlint-disable-next-line typescript/no-non-null-assertion -- successful provider registration owns the shared tools99
if (layer.servers.isEmpty()) disposal = layer.disposeTools!()100
}101
}, { label: `mcpResources.provider(${server})` })102
}.bind(this), `mcpResources.register(${server})`)103
// oxlint-disable-next-line typescript/no-misused-promises -- visibility cleanup is synchronous; Cordis retains pending fiber disposal104
return dispose105
}107
/** Own one scope's tools independently of its configured server plugins. */108
private registerTools(scope: ScopeKey | undefined): () => void | Promise<void> {109
const ctx = this.selfCtx110
return ctx.effect(function* (this: McpResourceRuntime) {111
let toolCtx = ctx112
if (scope !== undefined) {113
const owned = createScope(ctx, scope)114
yield owned.rawDispose115
toolCtx = owned.ctx116
}117
yield registerResourceTools(toolCtx, (server, request, exec) => this.request(server, request, exec))118
}.bind(this), 'mcpResources.tools')119
}121
/** Resolve the caller-visible server before starting any network operation. */122
private request(server: string, request: McpResourceRequest, exec: ToolExecution): Promise<JsonValue> {123
const provider = this.layers.merge(exec.agent, layer => layer.servers).get(server)124
if (!provider) throw new Error(`MCP resource server "${server}" is unavailable in this agent's scope`)125
return provider.request(request, exec)126
}127
}129
export default McpResourceRuntime