Commit a489726
authored
Refactor: Consolidate function clustering — Docker utilities, stdlib reimplementation, and string truncation (#1350)
Analysis of 67 non-test Go files identified 5 refactoring opportunities:
misplaced Docker logic, near-duplicate truncation utilities, repeated
logger boilerplate, and a trivial stdlib reimplementation. This PR
addresses 3 high-impact opportunities with minimal code changes.
## Changes
- **Removed `containsEqual()` helper** — Replaced with
`strings.Contains(s, "=")` (stdlib)
- Function manually iterated through string to find `'='` character
- Already used `strings.Contains()` elsewhere in same file
- **Extracted Docker utilities to `internal/dockerutil`**
- Moved `expandDockerEnvArgs()` from `internal/mcp/connection.go`
- Avoids circular dependency between `mcp` and `launcher` packages
- Tests moved to `internal/dockerutil/env_test.go`
- **Created `internal/strutil` package for string truncation**
- Consolidates 4 duplicate truncation patterns across codebase:
- Inline truncation in `connection.go` (500 chars + "... (truncated)")
- `truncateAndSanitize()` in `logger/rpc_helpers.go`
- `TruncateSecret()` in `logger/sanitize` (special case, not modified)
- `TruncateSessionID()` in `auth/header` (special case, not modified)
- Provides `Truncate()` and `TruncateWithSuffix()` with consistent edge
case handling
```go
// Before: manual truncation with inconsistent patterns
bodyPreview := string(body)
if len(bodyPreview) > 500 {
bodyPreview = bodyPreview[:500] + "... (truncated)"
}
// After: unified utility
bodyPreview := strutil.TruncateWithSuffix(string(body), 500, "... (truncated)")
```
## Out of Scope
**Logger boilerplate consolidation** deferred — ~86 wrapper functions
across 6 logger types follow consistent patterns for type safety.
Refactoring would require architectural changes (generics/reflection)
with marginal benefit vs risk.
> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `example.com`
> - Triggering command: `/tmp/go-build3509919732/b284/launcher.test
/tmp/go-build3509919732/b284/launcher.test
-test.testlogfile=/tmp/go-build3509919732/b284/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true --abbrev-ref HEAD
ache/uv/0.10.5/x86_64/git
64/pkg/tool/linu/opt/hostedtoolcache/go/1.25.6/x64/pkg/tool/linux_amd64/vet
base64 ache/Python/3.12/tmp/go-build2383556780/b045/vet.cfg base64 -d`
(dns block)
> - `invalid-host-that-does-not-exist-12345.com`
> - Triggering command: `/tmp/go-build3509919732/b266/config.test
/tmp/go-build3509919732/b266/config.test
-test.testlogfile=/tmp/go-build3509919732/b266/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true
home/REDACTED/.nvm-n1 /sys/fs/cgroup 64/bin/git
64/pkg/tool/linu/opt/hostedtoolcache/go/1.25.6/x64/pkg/tool/linux_amd64/vet
base64 ache/uv/0.10.5/x/tmp/go-build2383556780/b066/vet.cfg base64 -d
rgo/bin/git iptables /home/REDACTED/.cargo/bin/git -t security
ache/Python/3.12/tmp/go-build2383556780/b228/vet.cfg git` (dns block)
> - `nonexistent.local`
> - Triggering command: `/tmp/go-build3509919732/b284/launcher.test
/tmp/go-build3509919732/b284/launcher.test
-test.testlogfile=/tmp/go-build3509919732/b284/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true --abbrev-ref HEAD
ache/uv/0.10.5/x86_64/git
64/pkg/tool/linu/opt/hostedtoolcache/go/1.25.6/x64/pkg/tool/linux_amd64/vet
base64 ache/Python/3.12/tmp/go-build2383556780/b045/vet.cfg base64 -d`
(dns block)
> - `slow.example.com`
> - Triggering command: `/tmp/go-build3509919732/b284/launcher.test
/tmp/go-build3509919732/b284/launcher.test
-test.testlogfile=/tmp/go-build3509919732/b284/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true --abbrev-ref HEAD
ache/uv/0.10.5/x86_64/git
64/pkg/tool/linu/opt/hostedtoolcache/go/1.25.6/x64/pkg/tool/linux_amd64/vet
base64 ache/Python/3.12/tmp/go-build2383556780/b045/vet.cfg base64 -d`
(dns block)
> - `this-host-does-not-exist-12345.com`
> - Triggering command: `/tmp/go-build3663849786/b001/mcp.test
/tmp/go-build3663849786/b001/mcp.test
-test.testlogfile=/tmp/go-build3663849786/b001/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true --abbrev-ref HEAD
p/bin/git --abbrev-ref HEAD /usr/bin/base64 base64 -d 64/bin/as base64
86_64/git /opt/hostedtoolcbase64 ache/go/1.25.6/x-d /usr/bin/git git`
(dns block)
> - Triggering command: `/tmp/go-build3509919732/b293/mcp.test
/tmp/go-build3509919732/b293/mcp.test
-test.testlogfile=/tmp/go-build3509919732/b293/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true ./internal/mcp
./internal/logge-nolocalimports /usr/bin/dirname-importcfg [:upper:] git
nfig/composer/ve-unreachable=false dirname /hom�� @v5.3.1/compiler.go
@v5.3.1/content.go x_amd64/compile s#\(.*\)\.\([^\.grep -e
/opt/hostedtoolc(create|run) x_amd64/compile` (dns block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/github/gh-aw-mcpg/settings/copilot/coding_agent)
(admins only)
>
> </details>
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
----
*This section details on the original issue you should resolve*
<issue_title>[refactor] Semantic Function Clustering Analysis —
Outliers, Near-Duplicates, and Consolidation Opportunities</issue_title>
<issue_description>## Overview
Analysis of **67 non-test Go source files** across `internal/`
catalogued **426 top-level functions** and identified 5 actionable
refactoring opportunities. The findings span misplaced functions (wrong
package), near-duplicate string-truncation utilities, repeated
boilerplate logger wrappers, and a trivial helper reimplementing stdlib.
The `internal/logger/` package is the most complex area (~120 functions
across 13 files) and contains the most structural repetition. The
`internal/mcp/connection.go` file contains Docker-specific logic that
belongs in `internal/launcher/`.
<details><summary><b>Full Report</b></summary>
## Analysis Metadata
| Metric | Value |
|--------|-------|
| Go files analyzed | 67 (non-test, `internal/` only) |
| Functions catalogued | 426 |
| Packages analysed | 16 |
| Outliers found | 2 |
| Near-duplicates detected | 3 clusters |
| Analysis date | 2026-02-23 |
---
## Identified Issues
### 1. 🗂️ Outlier: Docker-specific functions in MCP connection file
**Severity**: Medium
**File**: `internal/mcp/connection.go` (lines 968–1005)
**Functions**: `expandDockerEnvArgs`, `containsEqual`
`connection.go` is responsible for MCP protocol transport (stdio, HTTP,
SSE). However it contains Docker-specific argument-expansion logic that
belongs conceptually in `internal/launcher/`:
```go
// internal/mcp/connection.go — Docker logic in an MCP transport file
func expandDockerEnvArgs(args []string) []string { ... }
func containsEqual(s string) bool { ... }
```
**Additional problem**: `containsEqual` reimplements
`strings.Contains(s, "=")` from the standard library. The entire
function body can be replaced with a one-liner.
**Recommendation**:
- Move `expandDockerEnvArgs` to `internal/launcher/launcher.go` (or a
new `internal/launcher/docker_args.go`), where Docker process arguments
are already assembled
- Remove `containsEqual` entirely and inline `strings.Contains(arg,
"=")` at the single call site
- **Estimated effort**: 30 minutes
- **Benefits**: `connection.go` stays protocol-focused; Docker
pre-processing stays near Docker process launch
---
### 2. 🗂️ Outlier: `applyAuthIfConfigured` in transport file instead of
auth file
**Severity**: Low
**File**: `internal/server/transport.go` (lines 59–65)
**Function**: `applyAuthIfConfigured`
The project already has a dedicated `internal/server/auth.go` that owns
`authMiddleware`. `applyAuthIfConfigured` is a thin conditional wrapper
around it:
```go
// internal/server/transport.go — auth helper split from auth.go
func applyAuthIfConfigured(apiKey string, handler http.HandlerFunc) http.HandlerFunc {
if apiKey != "" {
return authMiddleware(apiKey, handler)
}
return handler
}
```
`applyAuthIfConfigured` is called from `http_helpers.go` and
`handlers.go` — neither of which is `transport.go`. The function's
natural home is `auth.go` alongside `authMiddleware`.
**Recommendation**:
- Move `applyAuthIfConfigured` from `transport.go` to `auth.go`
- **Estimated effort**: 15 minutes
- **Benefits**: All auth-gating logic lives in one file; `transport.go`
becomes purely about HTTP server/transport construction
---
### 3. 🔁 Near-duplicate: `TruncateSessionID` vs `TruncateSecret`
**Severity**: Low
**Files**: `internal/auth/header.go:172`,
`internal/logger/sanitize/sanitize.go:75`
Both functions truncate a string for safe logging, but with slightly
different semantics:
```go
// internal/auth/header.go
func TruncateSessionID(sessionID string) string {
if sessionID == "" { return "(none)" }
if len(sessionID) <= 8 { return sessionID }
return sessionID[:8] + "..."
}
// internal/logger/sanitize/sanitize.go
func TruncateSecret(input string) string {
if len(input) > 4 { return input[:4] + "..." }
else if len(input) > 0 { return "..." }
return ""
}
```
The two functions differ in: prefix length (8 vs 4), empty-string return
value (`"(none)"` vs `""`), and short-input handling. Despite different
semantics, both exist to prevent sensitive data from appearing in logs.
**Recommendation**:
- Consider whether `TruncateSessionID` could call `TruncateSecret` (or a
parameterised version) to reduce divergence
- At minimum, add a cross-reference comment in each function pointing to
the other
- A longer-term option: add a `TruncateWithLength(s string, n int,
fallback string) string` helper in `sanitize/` and have both functions
delegate to it
- **Estimated effort**: 1–2 hours
- **Benefits**: Single source of truth for truncation semantics; easier
to change policy (e.g., 8 chars → 12 chars) in one place
---
### 4. 🔁 Structural repetition: `LogX` / `LogXMd` / `LogXWithServer`
quadruplets
**Severity**: Medium
**Files**: `internal/logger/file_logger....
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes #13367 files changed
Lines changed: 272 additions & 132 deletions
File tree
- internal
- dockerutil
- logger
- mcp
- strutil
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
23 | 24 | | |
24 | 25 | | |
25 | 26 | | |
| |||
44 | 45 | | |
45 | 46 | | |
46 | 47 | | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
| 48 | + | |
51 | 49 | | |
52 | 50 | | |
53 | 51 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
14 | 13 | | |
15 | 14 | | |
16 | 15 | | |
17 | 16 | | |
18 | 17 | | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
93 | 94 | | |
94 | 95 | | |
95 | 96 | | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
| 97 | + | |
100 | 98 | | |
101 | 99 | | |
102 | 100 | | |
| |||
314 | 312 | | |
315 | 313 | | |
316 | 314 | | |
317 | | - | |
| 315 | + | |
318 | 316 | | |
319 | 317 | | |
320 | 318 | | |
| |||
965 | 963 | | |
966 | 964 | | |
967 | 965 | | |
968 | | - | |
969 | | - | |
970 | | - | |
971 | | - | |
972 | | - | |
973 | | - | |
974 | | - | |
975 | | - | |
976 | | - | |
977 | | - | |
978 | | - | |
979 | | - | |
980 | | - | |
981 | | - | |
982 | | - | |
983 | | - | |
984 | | - | |
985 | | - | |
986 | | - | |
987 | | - | |
988 | | - | |
989 | | - | |
990 | | - | |
991 | | - | |
992 | | - | |
993 | | - | |
994 | | - | |
995 | | - | |
996 | | - | |
997 | | - | |
998 | | - | |
999 | | - | |
1000 | | - | |
1001 | | - | |
1002 | | - | |
1003 | 966 | | |
1004 | 967 | | |
1005 | 968 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
11 | 10 | | |
12 | 11 | | |
13 | 12 | | |
| |||
131 | 130 | | |
132 | 131 | | |
133 | 132 | | |
134 | | - | |
135 | | - | |
136 | | - | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
152 | | - | |
153 | | - | |
154 | | - | |
155 | | - | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
166 | | - | |
167 | | - | |
168 | | - | |
169 | | - | |
170 | | - | |
171 | | - | |
172 | | - | |
173 | | - | |
174 | | - | |
175 | | - | |
176 | | - | |
177 | | - | |
178 | | - | |
179 | | - | |
180 | | - | |
181 | | - | |
182 | | - | |
183 | | - | |
184 | | - | |
185 | | - | |
186 | | - | |
187 | | - | |
188 | | - | |
189 | | - | |
190 | | - | |
191 | | - | |
192 | | - | |
193 | | - | |
194 | | - | |
195 | | - | |
196 | | - | |
197 | | - | |
198 | | - | |
199 | | - | |
200 | | - | |
201 | | - | |
202 | | - | |
203 | | - | |
204 | | - | |
205 | | - | |
206 | | - | |
207 | | - | |
208 | | - | |
209 | | - | |
210 | | - | |
211 | | - | |
212 | | - | |
213 | | - | |
214 | | - | |
215 | | - | |
216 | | - | |
217 | | - | |
218 | | - | |
219 | | - | |
220 | 133 | | |
221 | 134 | | |
222 | 135 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
0 commit comments