Skip to content

Commit dc759e8

Browse files
Merge branch 'main' into patch-1
2 parents 04c34b8 + dd239d8 commit dc759e8

File tree

13 files changed

+262
-6
lines changed

13 files changed

+262
-6
lines changed

internal/ghmcp/server.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"github.com/github/github-mcp-server/pkg/inventory"
1919
"github.com/github/github-mcp-server/pkg/lockdown"
2020
mcplog "github.com/github/github-mcp-server/pkg/log"
21+
"github.com/github/github-mcp-server/pkg/observability"
22+
"github.com/github/github-mcp-server/pkg/observability/metrics"
2123
"github.com/github/github-mcp-server/pkg/raw"
2224
"github.com/github/github-mcp-server/pkg/scopes"
2325
"github.com/github/github-mcp-server/pkg/translations"
@@ -116,6 +118,10 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
116118
featureChecker := createFeatureChecker(cfg.EnabledFeatures)
117119

118120
// Create dependencies for tool handlers
121+
obs, err := observability.NewExporters(cfg.Logger, metrics.NewNoopMetrics())
122+
if err != nil {
123+
return nil, fmt.Errorf("failed to create observability exporters: %w", err)
124+
}
119125
deps := github.NewBaseDeps(
120126
clients.rest,
121127
clients.gql,
@@ -128,6 +134,7 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
128134
},
129135
cfg.ContentWindowSize,
130136
featureChecker,
137+
obs,
131138
)
132139
// Build and register the tool/resource/prompt inventory
133140
inventoryBuilder := github.NewInventory(cfg.Translator).

pkg/github/context_tools_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,10 @@ func Test_GetMe(t *testing.T) {
9696
t.Run(tc.name, func(t *testing.T) {
9797
var deps ToolDependencies
9898
if tc.clientErr != "" {
99-
deps = stubDeps{clientFn: stubClientFnErr(tc.clientErr)}
99+
deps = stubDeps{clientFn: stubClientFnErr(tc.clientErr), obsv: stubExporters()}
100100
} else {
101-
deps = BaseDeps{Client: github.NewClient(tc.mockedClient)}
101+
obs := stubExporters()
102+
deps = BaseDeps{Client: github.NewClient(tc.mockedClient), Obsv: obs}
102103
}
103104
handler := serverTool.Handler(deps)
104105

@@ -304,7 +305,7 @@ func Test_GetTeams(t *testing.T) {
304305
{
305306
name: "getting client fails",
306307
makeDeps: func() ToolDependencies {
307-
return stubDeps{clientFn: stubClientFnErr("expected test error")}
308+
return stubDeps{clientFn: stubClientFnErr("expected test error"), obsv: stubExporters()}
308309
},
309310
requestArgs: map[string]any{},
310311
expectToolError: true,
@@ -315,6 +316,7 @@ func Test_GetTeams(t *testing.T) {
315316
makeDeps: func() ToolDependencies {
316317
return BaseDeps{
317318
Client: github.NewClient(httpClientUserFails()),
319+
Obsv: stubExporters(),
318320
}
319321
},
320322
requestArgs: map[string]any{},
@@ -327,6 +329,7 @@ func Test_GetTeams(t *testing.T) {
327329
return stubDeps{
328330
clientFn: stubClientFnFromHTTP(httpClientWithUser()),
329331
gqlClientFn: stubGQLClientFnErr("GraphQL client error"),
332+
obsv: stubExporters(),
330333
}
331334
},
332335
requestArgs: map[string]any{},
@@ -469,7 +472,7 @@ func Test_GetTeamMembers(t *testing.T) {
469472
},
470473
{
471474
name: "getting GraphQL client fails",
472-
deps: stubDeps{gqlClientFn: stubGQLClientFnErr("GraphQL client error")},
475+
deps: stubDeps{gqlClientFn: stubGQLClientFnErr("GraphQL client error"), obsv: stubExporters()},
473476
requestArgs: map[string]any{
474477
"org": "testorg",
475478
"team_slug": "testteam",

pkg/github/dependencies.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"log/slog"
78
"net/http"
89
"os"
910

1011
ghcontext "github.com/github/github-mcp-server/pkg/context"
1112
"github.com/github/github-mcp-server/pkg/http/transport"
1213
"github.com/github/github-mcp-server/pkg/inventory"
1314
"github.com/github/github-mcp-server/pkg/lockdown"
15+
"github.com/github/github-mcp-server/pkg/observability"
16+
"github.com/github/github-mcp-server/pkg/observability/metrics"
1417
"github.com/github/github-mcp-server/pkg/raw"
1518
"github.com/github/github-mcp-server/pkg/scopes"
1619
"github.com/github/github-mcp-server/pkg/translations"
@@ -94,6 +97,14 @@ type ToolDependencies interface {
9497

9598
// IsFeatureEnabled checks if a feature flag is enabled.
9699
IsFeatureEnabled(ctx context.Context, flagName string) bool
100+
101+
// Logger returns the structured logger, optionally enriched with
102+
// request-scoped data from ctx. Integrators provide their own slog.Handler
103+
// to control where logs are sent.
104+
Logger(ctx context.Context) *slog.Logger
105+
106+
// Metrics returns the metrics client
107+
Metrics(ctx context.Context) metrics.Metrics
97108
}
98109

99110
// BaseDeps is the standard implementation of ToolDependencies for the local server.
@@ -113,6 +124,9 @@ type BaseDeps struct {
113124

114125
// Feature flag checker for runtime checks
115126
featureChecker inventory.FeatureFlagChecker
127+
128+
// Observability exporters (includes logger)
129+
Obsv observability.Exporters
116130
}
117131

118132
// Compile-time assertion to verify that BaseDeps implements the ToolDependencies interface.
@@ -128,6 +142,7 @@ func NewBaseDeps(
128142
flags FeatureFlags,
129143
contentWindowSize int,
130144
featureChecker inventory.FeatureFlagChecker,
145+
obsv observability.Exporters,
131146
) *BaseDeps {
132147
return &BaseDeps{
133148
Client: client,
@@ -138,6 +153,7 @@ func NewBaseDeps(
138153
Flags: flags,
139154
ContentWindowSize: contentWindowSize,
140155
featureChecker: featureChecker,
156+
Obsv: obsv,
141157
}
142158
}
143159

@@ -170,6 +186,16 @@ func (d BaseDeps) GetFlags(_ context.Context) FeatureFlags { return d.Flags }
170186
// GetContentWindowSize implements ToolDependencies.
171187
func (d BaseDeps) GetContentWindowSize() int { return d.ContentWindowSize }
172188

189+
// Logger implements ToolDependencies.
190+
func (d BaseDeps) Logger(_ context.Context) *slog.Logger {
191+
return d.Obsv.Logger()
192+
}
193+
194+
// Metrics implements ToolDependencies.
195+
func (d BaseDeps) Metrics(ctx context.Context) metrics.Metrics {
196+
return d.Obsv.Metrics(ctx)
197+
}
198+
173199
// IsFeatureEnabled checks if a feature flag is enabled.
174200
// Returns false if the feature checker is nil, flag name is empty, or an error occurs.
175201
// This allows tools to conditionally change behavior based on feature flags.
@@ -247,6 +273,9 @@ type RequestDeps struct {
247273

248274
// Feature flag checker for runtime checks
249275
featureChecker inventory.FeatureFlagChecker
276+
277+
// Observability exporters (includes logger)
278+
obsv observability.Exporters
250279
}
251280

252281
// NewRequestDeps creates a RequestDeps with the provided clients and configuration.
@@ -258,6 +287,7 @@ func NewRequestDeps(
258287
t translations.TranslationHelperFunc,
259288
contentWindowSize int,
260289
featureChecker inventory.FeatureFlagChecker,
290+
obsv observability.Exporters,
261291
) *RequestDeps {
262292
return &RequestDeps{
263293
apiHosts: apiHosts,
@@ -267,6 +297,7 @@ func NewRequestDeps(
267297
T: t,
268298
ContentWindowSize: contentWindowSize,
269299
featureChecker: featureChecker,
300+
obsv: obsv,
270301
}
271302
}
272303

@@ -374,6 +405,16 @@ func (d *RequestDeps) GetFlags(ctx context.Context) FeatureFlags {
374405
// GetContentWindowSize implements ToolDependencies.
375406
func (d *RequestDeps) GetContentWindowSize() int { return d.ContentWindowSize }
376407

408+
// Logger implements ToolDependencies.
409+
func (d *RequestDeps) Logger(_ context.Context) *slog.Logger {
410+
return d.obsv.Logger()
411+
}
412+
413+
// Metrics implements ToolDependencies.
414+
func (d *RequestDeps) Metrics(ctx context.Context) metrics.Metrics {
415+
return d.obsv.Metrics(ctx)
416+
}
417+
377418
// IsFeatureEnabled checks if a feature flag is enabled.
378419
func (d *RequestDeps) IsFeatureEnabled(ctx context.Context, flagName string) bool {
379420
if d.featureChecker == nil || flagName == "" {

pkg/github/dependencies_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@ package github_test
33
import (
44
"context"
55
"errors"
6+
"log/slog"
67
"testing"
78

89
"github.com/github/github-mcp-server/pkg/github"
10+
"github.com/github/github-mcp-server/pkg/observability"
11+
"github.com/github/github-mcp-server/pkg/observability/metrics"
912
"github.com/github/github-mcp-server/pkg/translations"
1013
"github.com/stretchr/testify/assert"
1114
)
1215

16+
func testExporters() observability.Exporters {
17+
obs, _ := observability.NewExporters(slog.New(slog.DiscardHandler), metrics.NewNoopMetrics())
18+
return obs
19+
}
20+
1321
func TestIsFeatureEnabled_WithEnabledFlag(t *testing.T) {
1422
t.Parallel()
1523

@@ -28,6 +36,7 @@ func TestIsFeatureEnabled_WithEnabledFlag(t *testing.T) {
2836
github.FeatureFlags{},
2937
0, // contentWindowSize
3038
checker, // featureChecker
39+
testExporters(),
3140
)
3241

3342
// Test enabled flag
@@ -52,6 +61,7 @@ func TestIsFeatureEnabled_WithoutChecker(t *testing.T) {
5261
github.FeatureFlags{},
5362
0, // contentWindowSize
5463
nil, // featureChecker (nil)
64+
testExporters(),
5565
)
5666

5767
// Should return false when checker is nil
@@ -76,6 +86,7 @@ func TestIsFeatureEnabled_EmptyFlagName(t *testing.T) {
7686
github.FeatureFlags{},
7787
0, // contentWindowSize
7888
checker, // featureChecker
89+
testExporters(),
7990
)
8091

8192
// Should return false for empty flag name
@@ -100,6 +111,7 @@ func TestIsFeatureEnabled_CheckerError(t *testing.T) {
100111
github.FeatureFlags{},
101112
0, // contentWindowSize
102113
checker, // featureChecker
114+
testExporters(),
103115
)
104116

105117
// Should return false and log error (not crash)

pkg/github/dynamic_tools_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func TestDynamicTools_EnableToolset(t *testing.T) {
136136
deps := DynamicToolDependencies{
137137
Server: server,
138138
Inventory: reg,
139-
ToolDeps: NewBaseDeps(nil, nil, nil, nil, translations.NullTranslationHelper, FeatureFlags{}, 0, nil),
139+
ToolDeps: NewBaseDeps(nil, nil, nil, nil, translations.NullTranslationHelper, FeatureFlags{}, 0, nil, stubExporters()),
140140
T: translations.NullTranslationHelper,
141141
}
142142

pkg/github/feature_flags_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ func TestHelloWorld_ConditionalBehavior_Featureflag(t *testing.T) {
104104
FeatureFlags{},
105105
0,
106106
checker,
107+
stubExporters(),
107108
)
108109

109110
// Get the tool and its handler
@@ -166,6 +167,7 @@ func TestHelloWorld_ConditionalBehavior_Config(t *testing.T) {
166167
FeatureFlags{InsidersMode: tt.insidersMode},
167168
0,
168169
nil,
170+
stubExporters(),
169171
)
170172

171173
// Get the tool and its handler

pkg/github/server_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"log/slog"
89
"net/http"
910
"testing"
1011
"time"
1112

1213
"github.com/github/github-mcp-server/pkg/lockdown"
14+
"github.com/github/github-mcp-server/pkg/observability"
15+
"github.com/github/github-mcp-server/pkg/observability/metrics"
1316
"github.com/github/github-mcp-server/pkg/raw"
1417
"github.com/github/github-mcp-server/pkg/translations"
1518
gogithub "github.com/google/go-github/v82/github"
@@ -30,6 +33,7 @@ type stubDeps struct {
3033
t translations.TranslationHelperFunc
3134
flags FeatureFlags
3235
contentWindowSize int
36+
obsv observability.Exporters
3337
}
3438

3539
func (s stubDeps) GetClient(ctx context.Context) (*gogithub.Client, error) {
@@ -60,8 +64,21 @@ func (s stubDeps) GetT() translations.TranslationHelperFunc { return s.
6064
func (s stubDeps) GetFlags(_ context.Context) FeatureFlags { return s.flags }
6165
func (s stubDeps) GetContentWindowSize() int { return s.contentWindowSize }
6266
func (s stubDeps) IsFeatureEnabled(_ context.Context, _ string) bool { return false }
67+
func (s stubDeps) Logger(_ context.Context) *slog.Logger {
68+
return s.obsv.Logger()
69+
}
70+
func (s stubDeps) Metrics(ctx context.Context) metrics.Metrics {
71+
return s.obsv.Metrics(ctx)
72+
}
6373

6474
// Helper functions to create stub client functions for error testing
75+
76+
// stubExporters returns a discard-logger + noop-metrics Exporters for tests.
77+
func stubExporters() observability.Exporters {
78+
obs, _ := observability.NewExporters(slog.New(slog.DiscardHandler), metrics.NewNoopMetrics())
79+
return obs
80+
}
81+
6582
func stubClientFnFromHTTP(httpClient *http.Client) func(context.Context) (*gogithub.Client, error) {
6683
return func(_ context.Context) (*gogithub.Client, error) {
6784
return gogithub.NewClient(httpClient), nil
@@ -125,7 +142,7 @@ func TestNewMCPServer_CreatesSuccessfully(t *testing.T) {
125142
InsidersMode: false,
126143
}
127144

128-
deps := stubDeps{}
145+
deps := stubDeps{obsv: stubExporters()}
129146

130147
// Build inventory
131148
inv, err := NewInventory(cfg.Translator).

pkg/http/server.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"github.com/github/github-mcp-server/pkg/http/oauth"
1818
"github.com/github/github-mcp-server/pkg/inventory"
1919
"github.com/github/github-mcp-server/pkg/lockdown"
20+
"github.com/github/github-mcp-server/pkg/observability"
21+
"github.com/github/github-mcp-server/pkg/observability/metrics"
2022
"github.com/github/github-mcp-server/pkg/scopes"
2123
"github.com/github/github-mcp-server/pkg/translations"
2224
"github.com/github/github-mcp-server/pkg/utils"
@@ -106,6 +108,11 @@ func RunHTTPServer(cfg ServerConfig) error {
106108

107109
featureChecker := createHTTPFeatureChecker()
108110

111+
obs, err := observability.NewExporters(logger, metrics.NewNoopMetrics())
112+
if err != nil {
113+
return fmt.Errorf("failed to create observability exporters: %w", err)
114+
}
115+
109116
deps := github.NewRequestDeps(
110117
apiHost,
111118
cfg.Version,
@@ -114,6 +121,7 @@ func RunHTTPServer(cfg ServerConfig) error {
114121
t,
115122
cfg.ContentWindowSize,
116123
featureChecker,
124+
obs,
117125
)
118126

119127
// Initialize the global tool scope map
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package metrics
2+
3+
import "time"
4+
5+
// Metrics is a backend-agnostic interface for emitting metrics.
6+
// Implementations can route to DataDog, log to slog, or discard (noop).
7+
type Metrics interface {
8+
Increment(key string, tags map[string]string)
9+
Counter(key string, tags map[string]string, value int64)
10+
Distribution(key string, tags map[string]string, value float64)
11+
DistributionMs(key string, tags map[string]string, value time.Duration)
12+
WithTags(tags map[string]string) Metrics
13+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package metrics
2+
3+
import "time"
4+
5+
// NoopMetrics is a no-op implementation of the Metrics interface.
6+
type NoopMetrics struct{}
7+
8+
var _ Metrics = (*NoopMetrics)(nil)
9+
10+
// NewNoopMetrics returns a new NoopMetrics.
11+
func NewNoopMetrics() *NoopMetrics {
12+
return &NoopMetrics{}
13+
}
14+
15+
func (n *NoopMetrics) Increment(_ string, _ map[string]string) {}
16+
func (n *NoopMetrics) Counter(_ string, _ map[string]string, _ int64) {}
17+
func (n *NoopMetrics) Distribution(_ string, _ map[string]string, _ float64) {}
18+
func (n *NoopMetrics) DistributionMs(_ string, _ map[string]string, _ time.Duration) {}
19+
func (n *NoopMetrics) WithTags(_ map[string]string) Metrics { return n }

0 commit comments

Comments
 (0)