|
@@ -67,6 +67,10 @@ export const Failed = NamedError.create("MCPFailed", {
|
|
|
name: Schema.String,
|
|
name: Schema.String,
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
|
|
|
|
|
+ name: Schema.String,
|
|
|
|
|
+}) {}
|
|
|
|
|
+
|
|
|
type MCPClient = Client
|
|
type MCPClient = Client
|
|
|
|
|
|
|
|
const StatusConnected = Schema.Struct({ status: Schema.Literal("connected") }).annotate({
|
|
const StatusConnected = Schema.Struct({ status: Schema.Literal("connected") }).annotate({
|
|
@@ -242,8 +246,8 @@ export interface Interface {
|
|
|
readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
|
|
readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
|
|
|
readonly resources: () => Effect.Effect<Record<string, ResourceInfo & { client: string }>>
|
|
readonly resources: () => Effect.Effect<Record<string, ResourceInfo & { client: string }>>
|
|
|
readonly add: (name: string, mcp: ConfigMCP.Info) => Effect.Effect<{ status: Record<string, Status> | Status }>
|
|
readonly add: (name: string, mcp: ConfigMCP.Info) => Effect.Effect<{ status: Record<string, Status> | Status }>
|
|
|
- readonly connect: (name: string) => Effect.Effect<void>
|
|
|
|
|
- readonly disconnect: (name: string) => Effect.Effect<void>
|
|
|
|
|
|
|
+ readonly connect: (name: string) => Effect.Effect<void, NotFoundError>
|
|
|
|
|
+ readonly disconnect: (name: string) => Effect.Effect<void, NotFoundError>
|
|
|
readonly getPrompt: (
|
|
readonly getPrompt: (
|
|
|
clientName: string,
|
|
clientName: string,
|
|
|
name: string,
|
|
name: string,
|
|
@@ -253,11 +257,11 @@ export interface Interface {
|
|
|
clientName: string,
|
|
clientName: string,
|
|
|
resourceUri: string,
|
|
resourceUri: string,
|
|
|
) => Effect.Effect<Awaited<ReturnType<MCPClient["readResource"]>> | undefined>
|
|
) => Effect.Effect<Awaited<ReturnType<MCPClient["readResource"]>> | undefined>
|
|
|
- readonly startAuth: (mcpName: string) => Effect.Effect<{ authorizationUrl: string; oauthState: string }>
|
|
|
|
|
- readonly authenticate: (mcpName: string) => Effect.Effect<Status>
|
|
|
|
|
- readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect<Status>
|
|
|
|
|
|
|
+ readonly startAuth: (mcpName: string) => Effect.Effect<{ authorizationUrl: string; oauthState: string }, NotFoundError>
|
|
|
|
|
+ readonly authenticate: (mcpName: string) => Effect.Effect<Status, NotFoundError>
|
|
|
|
|
+ readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect<Status, NotFoundError>
|
|
|
readonly removeAuth: (mcpName: string) => Effect.Effect<void>
|
|
readonly removeAuth: (mcpName: string) => Effect.Effect<void>
|
|
|
- readonly supportsOAuth: (mcpName: string) => Effect.Effect<boolean>
|
|
|
|
|
|
|
+ readonly supportsOAuth: (mcpName: string) => Effect.Effect<boolean, NotFoundError>
|
|
|
readonly hasStoredTokens: (mcpName: string) => Effect.Effect<boolean>
|
|
readonly hasStoredTokens: (mcpName: string) => Effect.Effect<boolean>
|
|
|
readonly getAuthStatus: (mcpName: string) => Effect.Effect<AuthStatus>
|
|
readonly getAuthStatus: (mcpName: string) => Effect.Effect<AuthStatus>
|
|
|
}
|
|
}
|
|
@@ -642,15 +646,12 @@ export const layer = Layer.effect(
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const connect = Effect.fn("MCP.connect")(function* (name: string) {
|
|
const connect = Effect.fn("MCP.connect")(function* (name: string) {
|
|
|
- const mcp = yield* getMcpConfig(name)
|
|
|
|
|
- if (!mcp) {
|
|
|
|
|
- log.error("MCP config not found or invalid", { name })
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const mcp = yield* requireMcpConfig(name)
|
|
|
yield* createAndStore(name, { ...mcp, enabled: true })
|
|
yield* createAndStore(name, { ...mcp, enabled: true })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const disconnect = Effect.fn("MCP.disconnect")(function* (name: string) {
|
|
const disconnect = Effect.fn("MCP.disconnect")(function* (name: string) {
|
|
|
|
|
+ yield* requireMcpConfig(name)
|
|
|
const s = yield* InstanceState.get(state)
|
|
const s = yield* InstanceState.get(state)
|
|
|
yield* closeClient(s, name)
|
|
yield* closeClient(s, name)
|
|
|
delete s.clients[name]
|
|
delete s.clients[name]
|
|
@@ -759,9 +760,14 @@ export const layer = Layer.effect(
|
|
|
return mcpConfig
|
|
return mcpConfig
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- const startAuth = Effect.fn("MCP.startAuth")(function* (mcpName: string) {
|
|
|
|
|
|
|
+ const requireMcpConfig = Effect.fnUntraced(function* (mcpName: string) {
|
|
|
const mcpConfig = yield* getMcpConfig(mcpName)
|
|
const mcpConfig = yield* getMcpConfig(mcpName)
|
|
|
- if (!mcpConfig) throw new Error(`MCP server ${mcpName} not found or disabled`)
|
|
|
|
|
|
|
+ if (!mcpConfig) return yield* new NotFoundError({ name: mcpName })
|
|
|
|
|
+ return mcpConfig
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const startAuth = Effect.fn("MCP.startAuth")(function* (mcpName: string) {
|
|
|
|
|
+ const mcpConfig = yield* requireMcpConfig(mcpName)
|
|
|
if (mcpConfig.type !== "remote") throw new Error(`MCP server ${mcpName} is not a remote server`)
|
|
if (mcpConfig.type !== "remote") throw new Error(`MCP server ${mcpName} is not a remote server`)
|
|
|
if (mcpConfig.oauth === false) throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`)
|
|
if (mcpConfig.oauth === false) throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`)
|
|
|
const url = remoteURL(mcpName, mcpConfig.url)
|
|
const url = remoteURL(mcpName, mcpConfig.url)
|
|
@@ -825,11 +831,9 @@ export const layer = Layer.effect(
|
|
|
const result = yield* startAuth(mcpName)
|
|
const result = yield* startAuth(mcpName)
|
|
|
if (!result.authorizationUrl) {
|
|
if (!result.authorizationUrl) {
|
|
|
const client = "client" in result ? result.client : undefined
|
|
const client = "client" in result ? result.client : undefined
|
|
|
- const mcpConfig = yield* getMcpConfig(mcpName)
|
|
|
|
|
- if (!mcpConfig) {
|
|
|
|
|
- yield* Effect.tryPromise(() => client?.close() ?? Promise.resolve()).pipe(Effect.ignore)
|
|
|
|
|
- return { status: "failed", error: "MCP config not found after auth" } as Status
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const mcpConfig = yield* requireMcpConfig(mcpName).pipe(
|
|
|
|
|
+ Effect.tapError(() => Effect.tryPromise(() => client?.close() ?? Promise.resolve()).pipe(Effect.ignore)),
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
const listed = client ? yield* defs(mcpName, client, mcpConfig.timeout) : undefined
|
|
const listed = client ? yield* defs(mcpName, client, mcpConfig.timeout) : undefined
|
|
|
if (!client || !listed) {
|
|
if (!client || !listed) {
|
|
@@ -880,6 +884,7 @@ export const layer = Layer.effect(
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const finishAuth = Effect.fn("MCP.finishAuth")(function* (mcpName: string, authorizationCode: string) {
|
|
const finishAuth = Effect.fn("MCP.finishAuth")(function* (mcpName: string, authorizationCode: string) {
|
|
|
|
|
+ yield* requireMcpConfig(mcpName)
|
|
|
const transport = pendingOAuthTransports.get(mcpName)
|
|
const transport = pendingOAuthTransports.get(mcpName)
|
|
|
if (!transport) throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`)
|
|
if (!transport) throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`)
|
|
|
|
|
|
|
@@ -898,8 +903,7 @@ export const layer = Layer.effect(
|
|
|
yield* auth.clearCodeVerifier(mcpName)
|
|
yield* auth.clearCodeVerifier(mcpName)
|
|
|
pendingOAuthTransports.delete(mcpName)
|
|
pendingOAuthTransports.delete(mcpName)
|
|
|
|
|
|
|
|
- const mcpConfig = yield* getMcpConfig(mcpName)
|
|
|
|
|
- if (!mcpConfig) return { status: "failed", error: "MCP config not found after auth" } as Status
|
|
|
|
|
|
|
+ const mcpConfig = yield* requireMcpConfig(mcpName)
|
|
|
|
|
|
|
|
return yield* createAndStore(mcpName, mcpConfig)
|
|
return yield* createAndStore(mcpName, mcpConfig)
|
|
|
})
|
|
})
|
|
@@ -912,8 +916,7 @@ export const layer = Layer.effect(
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const supportsOAuth = Effect.fn("MCP.supportsOAuth")(function* (mcpName: string) {
|
|
const supportsOAuth = Effect.fn("MCP.supportsOAuth")(function* (mcpName: string) {
|
|
|
- const mcpConfig = yield* getMcpConfig(mcpName)
|
|
|
|
|
- if (!mcpConfig) return false
|
|
|
|
|
|
|
+ const mcpConfig = yield* requireMcpConfig(mcpName)
|
|
|
return mcpConfig.type === "remote" && mcpConfig.oauth !== false
|
|
return mcpConfig.type === "remote" && mcpConfig.oauth !== false
|
|
|
})
|
|
})
|
|
|
|
|
|