feat(plugin): expose app metadata (#38179)

This commit is contained in:
Dax 2026-07-21 18:16:25 -04:00 committed by GitHub
commit 8de40be6ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 477 additions and 286 deletions

View file

@ -12,6 +12,7 @@ import {
ElicitationCompleteNotificationSchema,
ElicitRequestSchema,
GetPromptResultSchema,
type Implementation,
type ElicitRequestFormParams,
type ElicitRequestParams,
type ElicitRequestURLParams,
@ -29,7 +30,6 @@ import {
} from "@modelcontextprotocol/sdk/types.js"
import { Cause, Effect, Exit, Schema } from "effect"
import { ConfigMCP } from "../config/mcp"
import { InstallationVersion } from "@opencode-ai/util/installation/version"
const DEFAULT_STARTUP_TIMEOUT = 30_000
const DEFAULT_CATALOG_TIMEOUT = 30_000
@ -185,6 +185,7 @@ export const connect = Effect.fnUntraced(function* (
// stored token (and a no-op redirect) surfaces an UnauthorizedError, which we map to needs_auth.
authProvider?: OAuthClientProvider,
elicitation?: ElicitationHandler,
clientInfo: Implementation = { name: "opencode", version: "unknown" },
) {
const transport: Transport = yield* Effect.gen(function* () {
if (config.type === "local") {
@ -209,7 +210,7 @@ export const connect = Effect.fnUntraced(function* (
})
})
const client = new Client(
{ name: "opencode", version: InstallationVersion },
clientInfo,
{
capabilities: {
...(elicitation ? { elicitation: { form: { applyDefaults: true }, url: {} } } : {}),

View file

@ -157,7 +157,17 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/MCP") {}
export const layer = Layer.effect(
export const Options = Schema.Struct({
clientInfo: Schema.optional(
Schema.Struct({
name: Schema.String,
version: Schema.String,
}),
),
})
export type Options = typeof Options.Type
export const layer = (options?: Options) => Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@ -498,7 +508,14 @@ export const layer = Layer.effect(
const authProvider = yield* connectProvider(entry)
// List tools as part of connect so a failure here marks the server failed rather than
// leaving it connected with a silently empty tool list and no path to recover.
const result = yield* MCPClient.connect(name, entry.config, location.directory, authProvider, elicitation).pipe(
const result = yield* MCPClient.connect(
name,
entry.config,
location.directory,
authProvider,
elicitation,
options?.clientInfo,
).pipe(
Effect.flatMap((connection) => connection.tools().pipe(Effect.map((tools) => ({ connection, tools })))),
Scope.provide(scope),
Effect.exit,
@ -772,11 +789,15 @@ export const layer = Layer.effect(
}),
)
export const node = makeLocationNode({
service: Service,
layer,
deps: [Config.node, Location.node, EventV2.node, Form.node, Integration.node, Credential.node],
})
export function configured(options?: Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [Config.node, Location.node, EventV2.node, Form.node, Integration.node, Credential.node],
})
}
export const node = configured()
// Schema `optional` strips undefined-valued properties on encode, so fields can assign
// optional properties directly instead of conditionally spreading them.