Skip to content

Commit f13e75d

Browse files
committed
Add API Host interface to resolve URLs dynamically
1 parent 886025a commit f13e75d

File tree

5 files changed

+133
-55
lines changed

5 files changed

+133
-55
lines changed

internal/ghmcp/server.go

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,32 @@ type githubClients struct {
3636
}
3737

3838
// createGitHubClients creates all the GitHub API clients needed by the server.
39-
func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHost) (*githubClients, error) {
39+
func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHostResolver) (*githubClients, error) {
40+
restURL, err := apiHost.BaseRESTURL(context.Background())
41+
if err != nil {
42+
return nil, fmt.Errorf("failed to get base REST URL: %w", err)
43+
}
44+
45+
uploadURL, err := apiHost.UploadURL(context.Background())
46+
if err != nil {
47+
return nil, fmt.Errorf("failed to get upload URL: %w", err)
48+
}
49+
50+
graphQLURL, err := apiHost.GraphqlURL(context.Background())
51+
if err != nil {
52+
return nil, fmt.Errorf("failed to get GraphQL URL: %w", err)
53+
}
54+
55+
rawURL, err := apiHost.RawURL(context.Background())
56+
if err != nil {
57+
return nil, fmt.Errorf("failed to get Raw URL: %w", err)
58+
}
59+
4060
// Construct REST client
4161
restClient := gogithub.NewClient(nil).WithAuthToken(cfg.Token)
4262
restClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", cfg.Version)
43-
restClient.BaseURL = apiHost.BaseRESTURL
44-
restClient.UploadURL = apiHost.UploadURL
63+
restClient.BaseURL = restURL
64+
restClient.UploadURL = uploadURL
4565

4666
// Construct GraphQL client
4767
// We use NewEnterpriseClient unconditionally since we already parsed the API host
@@ -51,10 +71,11 @@ func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHost) (*gi
5171
Token: cfg.Token,
5272
},
5373
}
54-
gqlClient := githubv4.NewEnterpriseClient(apiHost.GraphqlURL.String(), gqlHTTPClient)
74+
75+
gqlClient := githubv4.NewEnterpriseClient(graphQLURL.String(), gqlHTTPClient)
5576

5677
// Create raw content client (shares REST client's HTTP transport)
57-
rawClient := raw.NewClient(restClient, apiHost.RawURL)
78+
rawClient := raw.NewClient(restClient, rawURL)
5879

5980
// Set up repo access cache for lockdown mode
6081
var repoAccessCache *lockdown.RepoAccessCache
@@ -78,7 +99,7 @@ func createGitHubClients(cfg github.MCPServerConfig, apiHost utils.APIHost) (*gi
7899
}
79100

80101
func NewStdioMCPServer(cfg github.MCPServerConfig) (*mcp.Server, error) {
81-
apiHost, err := utils.ParseAPIHost(cfg.Host)
102+
apiHost, err := utils.NewAPIHost(cfg.Host)
82103
if err != nil {
83104
return nil, fmt.Errorf("failed to parse API host: %w", err)
84105
}
@@ -308,13 +329,18 @@ func addUserAgentsMiddleware(cfg github.MCPServerConfig, restClient *gogithub.Cl
308329
// fetchTokenScopesForHost fetches the OAuth scopes for a token from the GitHub API.
309330
// It constructs the appropriate API host URL based on the configured host.
310331
func fetchTokenScopesForHost(ctx context.Context, token, host string) ([]string, error) {
311-
apiHost, err := utils.ParseAPIHost(host)
332+
apiHost, err := utils.NewAPIHost(host)
312333
if err != nil {
313334
return nil, fmt.Errorf("failed to parse API host: %w", err)
314335
}
315336

337+
baseRestURL, err := apiHost.BaseRESTURL(ctx)
338+
if err != nil {
339+
return nil, fmt.Errorf("failed to get base REST URL: %w", err)
340+
}
341+
316342
fetcher := scopes.NewFetcher(scopes.FetcherOptions{
317-
APIHost: apiHost.BaseRESTURL.String(),
343+
APIHost: baseRestURL.String(),
318344
})
319345

320346
return fetcher.FetchTokenScopes(ctx, token)

pkg/github/dependencies.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ type RequestDeps struct {
213213
RepoAccessCache *lockdown.RepoAccessCache
214214

215215
// Static dependencies
216-
apiHosts *utils.APIHost
216+
apiHosts utils.APIHostResolver
217217
version string
218218
lockdownMode bool
219219
RepoAccessOpts []lockdown.RepoAccessOption
@@ -224,7 +224,7 @@ type RequestDeps struct {
224224

225225
// NewRequestDeps creates a RequestDeps with the provided clients and configuration.
226226
func NewRequestDeps(
227-
apiHosts *utils.APIHost,
227+
apiHosts utils.APIHostResolver,
228228
version string,
229229
lockdownMode bool,
230230
repoAccessOpts []lockdown.RepoAccessOption,
@@ -252,11 +252,20 @@ func (d *RequestDeps) GetClient(ctx context.Context) (*gogithub.Client, error) {
252252
// extract the token from the context
253253
token, _ := ghcontext.GetTokenInfo(ctx)
254254

255+
baseRestURL, err := d.apiHosts.BaseRESTURL(ctx)
256+
if err != nil {
257+
return nil, fmt.Errorf("failed to get base REST URL: %w", err)
258+
}
259+
uploadURL, err := d.apiHosts.UploadURL(ctx)
260+
if err != nil {
261+
return nil, fmt.Errorf("failed to get upload URL: %w", err)
262+
}
263+
255264
// Construct REST client
256265
restClient := gogithub.NewClient(nil).WithAuthToken(token)
257266
restClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", d.version)
258-
restClient.BaseURL = d.apiHosts.BaseRESTURL
259-
restClient.UploadURL = d.apiHosts.UploadURL
267+
restClient.BaseURL = baseRestURL
268+
restClient.UploadURL = uploadURL
260269
return restClient, nil
261270
}
262271

@@ -277,7 +286,13 @@ func (d *RequestDeps) GetGQLClient(ctx context.Context) (*githubv4.Client, error
277286
Token: token,
278287
},
279288
}
280-
gqlClient := githubv4.NewEnterpriseClient(d.apiHosts.GraphqlURL.String(), gqlHTTPClient)
289+
290+
graphqlURL, err := d.apiHosts.GraphqlURL(ctx)
291+
if err != nil {
292+
return nil, fmt.Errorf("failed to get GraphQL URL: %w", err)
293+
}
294+
295+
gqlClient := githubv4.NewEnterpriseClient(graphqlURL.String(), gqlHTTPClient)
281296
d.GQLClient = gqlClient
282297
return gqlClient, nil
283298
}
@@ -293,7 +308,12 @@ func (d *RequestDeps) GetRawClient(ctx context.Context) (*raw.Client, error) {
293308
return nil, err
294309
}
295310

296-
rawClient := raw.NewClient(client, d.apiHosts.RawURL)
311+
rawURL, err := d.apiHosts.RawURL(ctx)
312+
if err != nil {
313+
return nil, fmt.Errorf("failed to get Raw URL: %w", err)
314+
}
315+
316+
rawClient := raw.NewClient(client, rawURL)
297317
d.RawClient = rawClient
298318

299319
return rawClient, nil

pkg/http/handler.go

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,53 +9,35 @@ import (
99
"github.com/github/github-mcp-server/pkg/http/headers"
1010
"github.com/github/github-mcp-server/pkg/http/middleware"
1111
"github.com/github/github-mcp-server/pkg/inventory"
12-
"github.com/github/github-mcp-server/pkg/lockdown"
1312
"github.com/github/github-mcp-server/pkg/translations"
14-
"github.com/github/github-mcp-server/pkg/utils"
1513
"github.com/modelcontextprotocol/go-sdk/mcp"
1614
)
1715

1816
type InventoryFactoryFunc func(r *http.Request) *inventory.Inventory
1917

2018
type HTTPMcpHandler struct {
2119
config *HTTPServerConfig
22-
apiHosts utils.APIHost
20+
deps github.ToolDependencies
2321
logger *slog.Logger
2422
t translations.TranslationHelperFunc
25-
repoAccessOpts []lockdown.RepoAccessOption
2623
inventoryFactoryFunc InventoryFactoryFunc
2724
}
2825

2926
func NewHTTPMcpHandler(cfg *HTTPServerConfig,
27+
deps github.ToolDependencies,
3028
t translations.TranslationHelperFunc,
31-
apiHosts *utils.APIHost,
32-
repoAccessOptions []lockdown.RepoAccessOption,
3329
logger *slog.Logger,
3430
inventoryFactory InventoryFactoryFunc) *HTTPMcpHandler {
3531
return &HTTPMcpHandler{
3632
config: cfg,
37-
apiHosts: *apiHosts,
33+
deps: deps,
3834
logger: logger,
3935
t: t,
40-
repoAccessOpts: repoAccessOptions,
4136
inventoryFactoryFunc: inventoryFactory,
4237
}
4338
}
4439

4540
func (s *HTTPMcpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
46-
// Set up repo access cache for lockdown mode
47-
deps := github.NewRequestDeps(
48-
&s.apiHosts,
49-
s.config.Version,
50-
s.config.LockdownMode,
51-
s.repoAccessOpts,
52-
s.t,
53-
github.FeatureFlags{
54-
LockdownMode: s.config.LockdownMode,
55-
},
56-
s.config.ContentWindowSize,
57-
)
58-
5941
inventory := s.inventoryFactoryFunc(r)
6042

6143
ghServer, err := github.NewMCPServer(&github.MCPServerConfig{
@@ -65,7 +47,7 @@ func (s *HTTPMcpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
6547
ContentWindowSize: s.config.ContentWindowSize,
6648
Logger: s.logger,
6749
RepoAccessTTL: s.config.RepoAccessCacheTTL,
68-
}, deps, inventory)
50+
}, s.deps, inventory)
6951
if err != nil {
7052
w.WriteHeader(http.StatusInternalServerError)
7153
}

pkg/http/server.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"syscall"
1212
"time"
1313

14+
"github.com/github/github-mcp-server/pkg/github"
1415
"github.com/github/github-mcp-server/pkg/lockdown"
1516
"github.com/github/github-mcp-server/pkg/translations"
1617
"github.com/github/github-mcp-server/pkg/utils"
@@ -70,7 +71,7 @@ func RunHTTPServer(cfg HTTPServerConfig) error {
7071
logger := slog.New(slogHandler)
7172
logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "lockdownEnabled", cfg.LockdownMode)
7273

73-
apiHost, err := utils.ParseAPIHost(cfg.Host)
74+
apiHost, err := utils.NewAPIHost(cfg.Host)
7475
if err != nil {
7576
return fmt.Errorf("failed to parse API host: %w", err)
7677
}
@@ -82,7 +83,19 @@ func RunHTTPServer(cfg HTTPServerConfig) error {
8283
repoAccessOpts = append(repoAccessOpts, lockdown.WithTTL(*cfg.RepoAccessCacheTTL))
8384
}
8485

85-
handler := NewHTTPMcpHandler(&cfg, t, &apiHost, repoAccessOpts, logger, DefaultInventoryFactory(&cfg, t, nil))
86+
deps := github.NewRequestDeps(
87+
apiHost,
88+
cfg.Version,
89+
cfg.LockdownMode,
90+
repoAccessOpts,
91+
t,
92+
github.FeatureFlags{
93+
LockdownMode: cfg.LockdownMode,
94+
},
95+
cfg.ContentWindowSize,
96+
)
97+
98+
handler := NewHTTPMcpHandler(&cfg, deps, t, logger, DefaultInventoryFactory(&cfg, t, nil))
8699

87100
r := chi.NewRouter()
88101
r.Mount("/", handler)

pkg/utils/api.go

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,55 @@
11
package utils
22

33
import (
4+
"context"
45
"fmt"
56
"net/http"
67
"net/url"
78
"strings"
89
"time"
910
)
1011

12+
type APIHostResolver interface {
13+
BaseRESTURL(ctx context.Context) (*url.URL, error)
14+
GraphqlURL(ctx context.Context) (*url.URL, error)
15+
UploadURL(ctx context.Context) (*url.URL, error)
16+
RawURL(ctx context.Context) (*url.URL, error)
17+
}
18+
1119
type APIHost struct {
12-
BaseRESTURL *url.URL
13-
GraphqlURL *url.URL
14-
UploadURL *url.URL
15-
RawURL *url.URL
20+
restURL *url.URL
21+
gqlURL *url.URL
22+
uploadURL *url.URL
23+
rawURL *url.URL
24+
}
25+
26+
var _ APIHostResolver = APIHost{}
27+
28+
func NewAPIHost(s string) (APIHostResolver, error) {
29+
a, err := parseAPIHost(s)
30+
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
return a, nil
36+
}
37+
38+
// APIHostResolver implementation
39+
func (a APIHost) BaseRESTURL(_ context.Context) (*url.URL, error) {
40+
return a.restURL, nil
41+
}
42+
43+
func (a APIHost) GraphqlURL(_ context.Context) (*url.URL, error) {
44+
return a.gqlURL, nil
45+
}
46+
47+
func (a APIHost) UploadURL(_ context.Context) (*url.URL, error) {
48+
return a.uploadURL, nil
49+
}
50+
51+
func (a APIHost) RawURL(_ context.Context) (*url.URL, error) {
52+
return a.rawURL, nil
1653
}
1754

1855
func newDotcomHost() (APIHost, error) {
@@ -37,10 +74,10 @@ func newDotcomHost() (APIHost, error) {
3774
}
3875

3976
return APIHost{
40-
BaseRESTURL: baseRestURL,
41-
GraphqlURL: gqlURL,
42-
UploadURL: uploadURL,
43-
RawURL: rawURL,
77+
restURL: baseRestURL,
78+
gqlURL: gqlURL,
79+
uploadURL: uploadURL,
80+
rawURL: rawURL,
4481
}, nil
4582
}
4683

@@ -76,10 +113,10 @@ func newGHECHost(hostname string) (APIHost, error) {
76113
}
77114

78115
return APIHost{
79-
BaseRESTURL: restURL,
80-
GraphqlURL: gqlURL,
81-
UploadURL: uploadURL,
82-
RawURL: rawURL,
116+
restURL: restURL,
117+
gqlURL: gqlURL,
118+
uploadURL: uploadURL,
119+
rawURL: rawURL,
83120
}, nil
84121
}
85122

@@ -128,10 +165,10 @@ func newGHESHost(hostname string) (APIHost, error) {
128165
}
129166

130167
return APIHost{
131-
BaseRESTURL: restURL,
132-
GraphqlURL: gqlURL,
133-
UploadURL: uploadURL,
134-
RawURL: rawURL,
168+
restURL: restURL,
169+
gqlURL: gqlURL,
170+
uploadURL: uploadURL,
171+
rawURL: rawURL,
135172
}, nil
136173
}
137174

@@ -159,7 +196,7 @@ func checkSubdomainIsolation(scheme, hostname string) bool {
159196
}
160197

161198
// Note that this does not handle ports yet, so development environments are out.
162-
func ParseAPIHost(s string) (APIHost, error) {
199+
func parseAPIHost(s string) (APIHost, error) {
163200
if s == "" {
164201
return newDotcomHost()
165202
}

0 commit comments

Comments
 (0)