RunStdioServer is not concurrent safe.
(cfg StdioServerConfig)
| 261 | |
| 262 | // RunStdioServer is not concurrent safe. |
| 263 | func RunStdioServer(cfg StdioServerConfig) error { |
| 264 | // OAuth login and a static token are mutually exclusive: they would |
| 265 | // disagree on how the token is sourced (lazy provider vs. static) and on |
| 266 | // scope filtering, so reject the ambiguous combination up front. |
| 267 | if cfg.OAuthManager != nil && cfg.Token != "" { |
| 268 | return fmt.Errorf("OAuthManager and a static Token are mutually exclusive: provide one or the other") |
| 269 | } |
| 270 | |
| 271 | // Create app context |
| 272 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) |
| 273 | defer stop() |
| 274 | |
| 275 | t, dumpTranslations := translations.TranslationHelper() |
| 276 | |
| 277 | var slogHandler slog.Handler |
| 278 | var logOutput io.Writer |
| 279 | if cfg.LogFilePath != "" { |
| 280 | file, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) |
| 281 | if err != nil { |
| 282 | return fmt.Errorf("failed to open log file: %w", err) |
| 283 | } |
| 284 | logOutput = file |
| 285 | slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelDebug}) |
| 286 | } else { |
| 287 | logOutput = os.Stderr |
| 288 | slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelInfo}) |
| 289 | } |
| 290 | logger := slog.New(slogHandler) |
| 291 | logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "readOnly", cfg.ReadOnly, "lockdownEnabled", cfg.LockdownMode) |
| 292 | |
| 293 | // Determine the scope set used to filter tools. Classic PATs expose their |
| 294 | // granted scopes via the API; OAuth uses the requested scopes (the default |
| 295 | // set hides nothing, a narrower explicit set filters accordingly). Other |
| 296 | // token types don't advertise scopes, so filtering is skipped. |
| 297 | var tokenScopes []string |
| 298 | switch { |
| 299 | case strings.HasPrefix(cfg.Token, "ghp_"): |
| 300 | fetchedScopes, err := fetchTokenScopesForHost(ctx, cfg.Token, cfg.Host) |
| 301 | if err != nil { |
| 302 | logger.Warn("failed to fetch token scopes, continuing without scope filtering", "error", err) |
| 303 | } else { |
| 304 | tokenScopes = fetchedScopes |
| 305 | logger.Info("token scopes fetched for filtering", "scopes", tokenScopes) |
| 306 | } |
| 307 | case cfg.OAuthManager != nil: |
| 308 | tokenScopes = cfg.OAuthScopes |
| 309 | logger.Info("using requested OAuth scopes for tool filtering", "scopes", tokenScopes) |
| 310 | default: |
| 311 | logger.Debug("skipping scope filtering for non-PAT token") |
| 312 | } |
| 313 | |
| 314 | // For OAuth, the token is resolved lazily: empty until the user authorizes |
| 315 | // on the first tool call, then refreshed for the rest of the session. |
| 316 | var tokenProvider func() string |
| 317 | if cfg.OAuthManager != nil { |
| 318 | tokenProvider = cfg.OAuthManager.AccessToken |
| 319 | } |
| 320 |