返回 DeepSeek-Reasonix
mcp_oauth.go
根目录 / desktop / mcp_oauth.go
1 package main
2
3 import (
4 "context"
5 "fmt"
6 "time"
7
8 "reasonix/internal/boot"
9 "reasonix/internal/config"
10 "reasonix/internal/mcpdiag"
11 "reasonix/internal/mcplaunch"
12 "reasonix/internal/netclient"
13 "reasonix/internal/plugin"
14 )
15
16 func (a *App) mcpLaunchSpec(root, name string) (plugin.Spec, error) {
17 cfg, err := config.LoadForRoot(root)
18 if err != nil {
19 return plugin.Spec{}, err
20 }
21 for _, entry := range cfg.Plugins {
22 if entry.Name == name {
23 return a.mcpLaunchSpecForEntryWithConfig(root, entry, cfg)
24 }
25 }
26 return plugin.Spec{}, fmt.Errorf("no configured MCP server named %q", name)
27 }
28
29 func (a *App) mcpLaunchSpecForEntry(root string, entry config.PluginEntry) (plugin.Spec, error) {
30 cfg, err := config.LoadForRoot(root)
31 if err != nil {
32 return plugin.Spec{}, err
33 }
34 return a.mcpLaunchSpecForEntryWithConfig(root, entry, cfg)
35 }
36
37 func (a *App) mcpLaunchSpecForEntryWithConfig(root string, entry config.PluginEntry, cfg *config.Config) (plugin.Spec, error) {
38 oauthHTTPClient, err := netclient.NewHTTPClient(cfg.NetworkProxySpec(), netclient.TransportOptions{})
39 if err != nil {
40 return plugin.Spec{}, err
41 }
42 specs := boot.PluginSpecsForRootWithOptions([]config.PluginEntry{entry}, root, boot.PluginSpecOptions{
43 DefaultCallTimeout: time.Duration(cfg.MCPCallTimeoutSeconds()) * time.Second,
44 LaunchManager: mcplaunch.ForWorkspace(config.ReasonixHomeDir(), root),
45 ConfigSource: "workspace_config", StateHome: config.ReasonixHomeDir(),
46 WriterRoots: cfg.WriteRootsForRoot(root), ForbidReadRoots: boot.RuntimeForbidReadRoots(cfg, root),
47 Network: cfg.Sandbox.Network, OAuthHTTPClient: oauthHTTPClient,
48 })
49 if len(specs) != 1 {
50 return plugin.Spec{}, fmt.Errorf("failed to build MCP server %q", entry.Name)
51 }
52 return specs[0], nil
53 }
54
55 var (
56 desktopAuthorizeHTTPMCP = plugin.AuthorizeHTTPMCP
57 desktopOpenMCPAuthorizationURL = func(a *App, rawURL string) error {
58 if a == nil || a.ctx == nil {
59 return fmt.Errorf("desktop runtime is not ready to open the authorization page")
60 }
61 a.nativeHost().OpenExternal(a.ctx, rawURL)
62 return nil
63 }
64 )
65
66 // AuthenticateMCPServer authorizes a remote MCP in private Reasonix state and
67 // reconnects every controller sharing the active host.
68 func (a *App) AuthenticateMCPServer(name string) error {
69 _, ctrl, root := a.activeMCPRuntime()
70 if ctrl == nil {
71 return fmt.Errorf("no active session")
72 }
73 entry, found, err := desktopEffectiveMCPServer(root, name)
74 if err != nil {
75 return err
76 }
77 if !found {
78 return fmt.Errorf("no configured MCP server named %q", name)
79 }
80 if !mcpdiag.CanUseHTTPMCPOAuth(entry.Type, entry.URL, mcpdiag.HasAuthConfig(entry.Headers, entry.Env, entry.URL)) {
81 return fmt.Errorf("MCP OAuth is only available for Streamable HTTP MCP servers without configured authentication")
82 }
83 spec, err := a.mcpLaunchSpecForEntry(root, entry)
84 if err != nil {
85 return err
86 }
87 ctx := context.Background()
88 if a.ctx != nil {
89 ctx = a.ctx
90 }
91 ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
92 defer cancel()
93 if err := desktopAuthorizeHTTPMCP(ctx, spec, func(rawURL string) error {
94 return desktopOpenMCPAuthorizationURL(a, rawURL)
95 }); err != nil {
96 return err
97 }
98 return a.ReconnectMCPServer(name)
99 }
100
101 // ClearMCPServerAuthentication removes Reasonix-owned auth state without
102 // signing out the third-party browser session or removing the server.
103 func (a *App) ClearMCPServerAuthentication(name string) error {
104 defer a.lockMCPMutation("clear-auth")()
105 tab, ctrl, root := a.activeMCPRuntime()
106 if tab == nil || ctrl == nil {
107 return fmt.Errorf("no active session")
108 }
109 host, releaseGates, err := a.lockMCPHostTurnGates("MCP server", ctrl)
110 if err != nil {
111 return err
112 }
113 defer releaseGates()
114 controllers := a.mcpControllersSharingHost(host, name, ctrl)
115 if err := ensureMCPServerDirectlyWritable(root, name); err != nil {
116 return err
117 }
118 entry, found, err := desktopEffectiveMCPServer(root, name)
119 if err != nil {
120 return err
121 }
122 if !found {
123 return fmt.Errorf("no configured MCP server named %q", name)
124 }
125 specs := boot.PluginSpecsForRootWithOptions([]config.PluginEntry{entry}, root, boot.PluginSpecOptions{
126 DefaultCallTimeout: 30 * time.Second, ConfigSource: string(entry.Source),
127 StateHome: config.ReasonixHomeDir(), Network: true,
128 })
129 if len(specs) == 1 {
130 if _, err := plugin.ClearHTTPMCPOAuth(specs[0]); err != nil {
131 return err
132 }
133 }
134 if _, _, _, err := config.ClearPluginAuthenticationInSourceForRoot(root, name); err != nil {
135 return err
136 }
137 disconnectMCPServerControllers(name, ctrl, controllers)
138 if host != nil {
139 host.ClearFailure(name)
140 }
141 // Auth state is authoritative configuration for in-flight controller builds.
142 a.bumpExtensionGeneration()
143 return nil
144 }
145
145 lines GO