@@ -18,7 +18,7 @@ import (
1818 "github.com/github/github-mcp-server/pkg/lockdown"
1919 mcplog "github.com/github/github-mcp-server/pkg/log"
2020 "github.com/github/github-mcp-server/pkg/raw"
21- "github.com/github/github-mcp-server/pkg/toolsets "
21+ "github.com/github/github-mcp-server/pkg/registry "
2222 "github.com/github/github-mcp-server/pkg/translations"
2323 gogithub "github.com/google/go-github/v79/github"
2424 "github.com/modelcontextprotocol/go-sdk/mcp"
@@ -69,146 +69,153 @@ type MCPServerConfig struct {
6969 RepoAccessTTL *time.Duration
7070}
7171
72- func NewMCPServer(cfg MCPServerConfig) (*mcp.Server, error) {
73- apiHost, err := parseAPIHost(cfg.Host)
74- if err != nil {
75- return nil, fmt.Errorf("failed to parse API host: %w", err)
76- }
72+ // githubClients holds all the GitHub API clients created for a server instance.
73+ type githubClients struct {
74+ rest *gogithub.Client
75+ gql *githubv4.Client
76+ gqlHTTP *http.Client // retained for middleware to modify transport
77+ raw *raw.Client
78+ repoAccess *lockdown.RepoAccessCache
79+ }
7780
78- // Construct our REST client
81+ // createGitHubClients creates all the GitHub API clients needed by the server.
82+ func createGitHubClients(cfg MCPServerConfig, apiHost apiHost) (*githubClients, error) {
83+ // Construct REST client
7984 restClient := gogithub.NewClient(nil).WithAuthToken(cfg.Token)
8085 restClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", cfg.Version)
8186 restClient.BaseURL = apiHost.baseRESTURL
8287 restClient.UploadURL = apiHost.uploadURL
8388
84- // Construct our GraphQL client
85- // We're using NewEnterpriseClient here unconditionally as opposed to NewClient because we already
86- // did the necessary API host parsing so that github.com will return the correct URL anyway.
89+ // Construct GraphQL client
90+ // We use NewEnterpriseClient unconditionally since we already parsed the API host
8791 gqlHTTPClient := &http.Client{
8892 Transport: &bearerAuthTransport{
8993 transport: http.DefaultTransport,
9094 token: cfg.Token,
9195 },
92- } // We're going to wrap the Transport later in beforeInit
93- gqlClient := githubv4.NewEnterpriseClient(apiHost.graphqlURL.String(), gqlHTTPClient)
94- repoAccessOpts := []lockdown.RepoAccessOption{}
95- if cfg.RepoAccessTTL != nil {
96- repoAccessOpts = append(repoAccessOpts, lockdown.WithTTL(*cfg.RepoAccessTTL))
9796 }
97+ gqlClient := githubv4.NewEnterpriseClient(apiHost.graphqlURL.String(), gqlHTTPClient)
9898
99- repoAccessLogger := cfg.Logger.With("component", "lockdown")
100- repoAccessOpts = append(repoAccessOpts, lockdown.WithLogger(repoAccessLogger))
99+ // Create raw content client (shares REST client's HTTP transport)
100+ rawClient := raw.NewClient(restClient, apiHost.rawURL)
101+
102+ // Set up repo access cache for lockdown mode
101103 var repoAccessCache *lockdown.RepoAccessCache
102104 if cfg.LockdownMode {
103- repoAccessCache = lockdown.GetInstance(gqlClient, repoAccessOpts...)
105+ opts := []lockdown.RepoAccessOption{
106+ lockdown.WithLogger(cfg.Logger.With("component", "lockdown")),
107+ }
108+ if cfg.RepoAccessTTL != nil {
109+ opts = append(opts, lockdown.WithTTL(*cfg.RepoAccessTTL))
110+ }
111+ repoAccessCache = lockdown.GetInstance(gqlClient, opts...)
104112 }
105113
106- // Determine enabled toolsets based on configuration:
107- // - nil means "use defaults" (unless dynamic mode without explicit toolsets)
108- // - empty slice means "no toolsets" (for dynamic mode to enable on demand)
109- // - explicit list means "use these toolsets"
110- var enabledToolsets []string
114+ return &githubClients{
115+ rest: restClient,
116+ gql: gqlClient,
117+ gqlHTTP: gqlHTTPClient,
118+ raw: rawClient,
119+ repoAccess: repoAccessCache,
120+ }, nil
121+ }
122+
123+ // resolveEnabledToolsets determines which toolsets should be enabled based on config.
124+ // Returns nil for "use defaults", empty slice for "none", or explicit list.
125+ func resolveEnabledToolsets(cfg MCPServerConfig) []string {
111126 if cfg.EnabledToolsets != nil {
112- enabledToolsets = cfg.EnabledToolsets
113- } else if cfg.DynamicToolsets {
114- // Dynamic mode with no toolsets specified: start with no toolsets enabled
115- // so users can enable them on demand via the dynamic tools
116- enabledToolsets = []string{}
127+ return cfg.EnabledToolsets
117128 }
118- // else: enabledToolsets stays nil, which means "use defaults" in WithToolsets
119-
120- // Generate instructions based on enabled toolsets
121- instructions := github.GenerateInstructions(enabledToolsets)
122-
123- getClient := func(_ context.Context) (*gogithub.Client, error) {
124- return restClient, nil // closing over client
129+ if cfg.DynamicToolsets {
130+ // Dynamic mode with no toolsets specified: start empty so users enable on demand
131+ return []string{}
125132 }
133+ // nil means "use defaults" in WithToolsets
134+ return nil
135+ }
126136
127- getGQLClient := func(_ context.Context) (*githubv4.Client, error) {
128- return gqlClient, nil // closing over client
137+ func NewMCPServer(cfg MCPServerConfig) (*mcp.Server, error) {
138+ apiHost, err := parseAPIHost(cfg.Host)
139+ if err != nil {
140+ return nil, fmt.Errorf("failed to parse API host: %w", err)
129141 }
130142
131- getRawClient := func(ctx context.Context) (*raw.Client, error) {
132- client, err := getClient(ctx)
133- if err != nil {
134- return nil, fmt.Errorf("failed to get GitHub client: %w", err)
135- }
136- return raw.NewClient(client, apiHost.rawURL), nil // closing over client
143+ clients, err := createGitHubClients(cfg, apiHost)
144+ if err != nil {
145+ return nil, fmt.Errorf("failed to create GitHub clients: %w", err)
137146 }
138147
148+ enabledToolsets := resolveEnabledToolsets(cfg)
149+
150+ // Create the MCP server
139151 ghServer := github.NewServer(cfg.Version, &mcp.ServerOptions{
140- Instructions: instructions,
141- Logger: cfg.Logger,
142- CompletionHandler: github.CompletionsHandler(getClient),
152+ Instructions: github.GenerateInstructions(enabledToolsets),
153+ Logger: cfg.Logger,
154+ CompletionHandler: github.CompletionsHandler(func(_ context.Context) (*gogithub.Client, error) {
155+ return clients.rest, nil
156+ }),
143157 })
144158
145159 // Add middlewares
146160 ghServer.AddReceivingMiddleware(addGitHubAPIErrorToContext)
147- ghServer.AddReceivingMiddleware(addUserAgentsMiddleware(cfg, restClient, gqlHTTPClient))
148-
149- // Create the dependencies struct for tool handlers
150- deps := github.ToolDependencies{
151- GetClient: getClient,
152- GetGQLClient: getGQLClient,
153- GetRawClient: getRawClient,
154- RepoAccessCache: repoAccessCache,
155- T: cfg.Translator,
156- Flags: github.FeatureFlags{LockdownMode: cfg.LockdownMode},
157- ContentWindowSize: cfg.ContentWindowSize,
158- }
159-
160- // Create toolset group with all tools, resources, and prompts (stateless)
161- r := github.NewRegistry(cfg.Translator)
162-
163- // Clean tool names (WithTools will resolve any deprecated aliases)
164- enabledTools := github.CleanTools(cfg.EnabledTools)
165-
166- // Apply filters based on configuration
167- // - WithDeprecatedToolAliases: adds backward compatibility aliases
168- // - WithReadOnly: filters out write tools when true
169- // - WithToolsets: nil=defaults, empty=none, handles "all"/"default" keywords
170- // - WithTools: additional tools that bypass toolset filtering (additive, resolves aliases)
171- // - WithFeatureChecker: filters based on feature flags
172- filteredReg := r.
173- WithDeprecatedToolAliases(github.DeprecatedToolAliases).
161+ ghServer.AddReceivingMiddleware(addUserAgentsMiddleware(cfg, clients.rest, clients.gqlHTTP))
162+
163+ // Create dependencies for tool handlers
164+ deps := github.NewBaseDeps(
165+ clients.rest,
166+ clients.gql,
167+ clients.raw,
168+ clients.repoAccess,
169+ cfg.Translator,
170+ github.FeatureFlags{LockdownMode: cfg.LockdownMode},
171+ cfg.ContentWindowSize,
172+ )
173+
174+ // Build and register the tool/resource/prompt registry
175+ registry := github.NewRegistry(cfg.Translator).
176+ WithDeprecatedAliases(github.DeprecatedToolAliases).
174177 WithReadOnly(cfg.ReadOnly).
175178 WithToolsets(enabledToolsets).
176- WithTools(enabledTools).
177- WithFeatureChecker(createFeatureChecker(cfg.EnabledFeatures))
179+ WithTools(github.CleanTools(cfg.EnabledTools)).
180+ WithFeatureChecker(createFeatureChecker(cfg.EnabledFeatures)).
181+ Build()
178182
179- // Warn about unrecognized toolset names (likely typos)
180- if unrecognized := filteredReg.UnrecognizedToolsets(); len(unrecognized) > 0 {
183+ if unrecognized := registry.UnrecognizedToolsets(); len(unrecognized) > 0 {
181184 fmt.Fprintf(os.Stderr, "Warning: unrecognized toolsets ignored: %s\n", strings.Join(unrecognized, ", "))
182185 }
183186
184- // Register all mcp functionality with the server
185- // Use background context for local server (no per-request actor context)
186- filteredReg.RegisterAll(context.Background(), ghServer, deps)
187+ // Register GitHub tools/resources/prompts from the registry.
188+ // In dynamic mode with no explicit toolsets, this is a no-op since enabledToolsets
189+ // is empty - users enable toolsets at runtime via the dynamic tools below (but can
190+ // enable toolsets or tools explicitly that do need registration).
191+ registry.RegisterAll(context.Background(), ghServer, deps)
187192
188- // Register dynamic toolset management if configured
189- // Dynamic tools get access to the filtered toolset group which tracks enabled state.
190- // ToolsForToolset() returns all tools for a toolset regardless of enabled status,
191- // so dynamic tools can enable any toolset at runtime.
193+ // Register dynamic toolset management tools (enable/disable) - these are separate
194+ // meta-tools that control the registry, not part of the registry itself
192195 if cfg.DynamicToolsets {
193- dynamicDeps := github.DynamicToolDependencies{
194- Server: ghServer,
195- Registry: filteredReg,
196- ToolDeps: deps,
197- T: cfg.Translator,
198- }
199- dynamicTools := github.DynamicTools(filteredReg)
200- for _, tool := range dynamicTools {
201- tool.RegisterFunc(ghServer, dynamicDeps)
202- }
196+ registerDynamicTools(ghServer, registry, deps, cfg.Translator)
203197 }
204198
205199 return ghServer, nil
206200}
207201
202+ // registerDynamicTools adds the dynamic toolset enable/disable tools to the server.
203+ func registerDynamicTools(server *mcp.Server, registry *registry.Registry, deps *github.BaseDeps, t translations.TranslationHelperFunc) {
204+ dynamicDeps := github.DynamicToolDependencies{
205+ Server: server,
206+ Registry: registry,
207+ ToolDeps: deps,
208+ T: t,
209+ }
210+ for _, tool := range github.DynamicTools(registry) {
211+ tool.RegisterFunc(server, dynamicDeps)
212+ }
213+ }
214+
208215// createFeatureChecker returns a FeatureFlagChecker that checks if a flag name
209216// is present in the provided list of enabled features. For the local server,
210217// this is populated from the --features CLI flag.
211- func createFeatureChecker(enabledFeatures []string) toolsets .FeatureFlagChecker {
218+ func createFeatureChecker(enabledFeatures []string) registry .FeatureFlagChecker {
212219 // Build a set for O(1) lookup
213220 featureSet := make(map[string]bool, len(enabledFeatures))
214221 for _, f := range enabledFeatures {
0 commit comments