Skip to content

Commit aee5074

Browse files
committed
add token estimation
1 parent 61b5868 commit aee5074

File tree

3 files changed

+96
-7
lines changed

3 files changed

+96
-7
lines changed

cmd/github-mcp-server/configure.go

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"sort"
78
"strings"
@@ -11,8 +12,10 @@ import (
1112
"github.com/github/github-mcp-server/pkg/github"
1213
"github.com/github/github-mcp-server/pkg/raw"
1314
gogithub "github.com/google/go-github/v74/github"
15+
"github.com/mark3labs/mcp-go/server"
1416
"github.com/shurcooL/githubv4"
1517
"github.com/spf13/cobra"
18+
"github.com/tiktoken-go/tokenizer"
1619
)
1720

1821
var configureCmd = &cobra.Command{
@@ -71,6 +74,9 @@ var (
7174
toolsetBadgeStyle = lipgloss.NewStyle().
7275
Foreground(lipgloss.Color("#84a5ecff"))
7376

77+
tokenBadgeStyle = lipgloss.NewStyle().
78+
Foreground(lipgloss.Color("#e8b75aff"))
79+
7480
successStyle = lipgloss.NewStyle().
7581
Foreground(lipgloss.Color("#00FF00")).
7682
Bold(true)
@@ -100,6 +106,7 @@ type toolInfo struct {
100106
description string
101107
toolsetName string
102108
isReadOnly bool
109+
tokenCount int // Estimated token count for this tool's definition
103110
}
104111

105112
type toolsetInfo struct {
@@ -122,9 +129,17 @@ type configureModel struct {
122129
confirmed bool
123130
viewportOffset int
124131
showWelcome bool
132+
encoder tokenizer.Codec // Tokenizer encoder for counting tokens
125133
}
126134

127135
func initialConfigureModel(toolsets []toolsetInfo) configureModel {
136+
// Initialize tokenizer
137+
enc, err := tokenizer.Get(tokenizer.Cl100kBase)
138+
if err != nil {
139+
// If tokenizer fails to initialize, continue without it
140+
enc = nil
141+
}
142+
128143
// Flatten all tools
129144
var allTools []toolInfo
130145
for _, ts := range toolsets {
@@ -144,6 +159,7 @@ func initialConfigureModel(toolsets []toolsetInfo) configureModel {
144159
width: 80,
145160
height: 24,
146161
showWelcome: true, // Start with welcome screen
162+
encoder: enc,
147163
}
148164
}
149165

@@ -352,10 +368,15 @@ func (m configureModel) View() string {
352368
visibleEnd = len(m.filteredTools)
353369
}
354370

371+
// Calculate selected count and total tokens
355372
selectedCount := 0
356-
for _, selected := range m.selected {
373+
totalTokens := 0
374+
for i, selected := range m.selected {
357375
if selected {
358376
selectedCount++
377+
if i < len(m.filteredTools) {
378+
totalTokens += m.filteredTools[i].tokenCount
379+
}
359380
}
360381
}
361382

@@ -392,10 +413,17 @@ func (m configureModel) View() string {
392413
category := toolsetBadgeStyle.Render(fmt.Sprintf("[%s]", tool.toolsetName))
393414
line += category
394415

416+
// Add token count if available
417+
if tool.tokenCount > 0 {
418+
tokenBadge := tokenBadgeStyle.Render(fmt.Sprintf(" ~%s tokens", formatTokenCount(tool.tokenCount)))
419+
line += tokenBadge
420+
}
421+
395422
// Add description (truncated if needed)
396423
desc := getFirstSentence(tool.description)
397-
if len(desc) > 60 {
398-
desc = desc[:57] + "..."
424+
maxDescLen := 45 // Reduced to make room for token count
425+
if len(desc) > maxDescLen {
426+
desc = desc[:maxDescLen-3] + "..."
399427
}
400428
line += " " + dimStyle.Render(desc)
401429

@@ -415,7 +443,11 @@ func (m configureModel) View() string {
415443
s.WriteString("\n")
416444

417445
// Footer with help
418-
s.WriteString(helpStyle.Render(fmt.Sprintf("Selected: %d tools", selectedCount)))
446+
footerInfo := fmt.Sprintf("Selected: %d tools", selectedCount)
447+
if m.encoder != nil && totalTokens > 0 {
448+
footerInfo += fmt.Sprintf(" • Estimated tokens: ~%s", formatTokenCount(totalTokens))
449+
}
450+
s.WriteString(helpStyle.Render(footerInfo))
419451
s.WriteString("\n")
420452
s.WriteString(helpStyle.Render("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"))
421453
s.WriteString("\n")
@@ -520,9 +552,11 @@ func (m configureModel) renderConfirmation() string {
520552

521553
// Get selected tools
522554
var selectedTools []string
555+
totalTokens := 0
523556
for i, selected := range m.selected {
524557
if selected && i < len(m.filteredTools) {
525558
selectedTools = append(selectedTools, m.filteredTools[i].name)
559+
totalTokens += m.filteredTools[i].tokenCount
526560
}
527561
}
528562

@@ -532,8 +566,16 @@ func (m configureModel) renderConfirmation() string {
532566
s.WriteString("\n")
533567
s.WriteString(successStyle.Render("✅ Configuration Complete!"))
534568
s.WriteString("\n\n")
569+
570+
// Add token summary
571+
if m.encoder != nil && totalTokens > 0 {
572+
s.WriteString(itemStyle.Render(fmt.Sprintf("📊 Total Estimated Tokens: ~%s", formatTokenCount(totalTokens))))
573+
s.WriteString("\n")
574+
s.WriteString(dimStyle.Render(" This is an approximation"))
575+
s.WriteString("\n\n")
576+
}
577+
535578
s.WriteString(titleStyle.Render("Selected Tools:"))
536-
s.WriteString("\n")
537579

538580
if len(selectedTools) == 0 {
539581
s.WriteString(dimStyle.Render(" (none - all tools will be enabled by default)"))
@@ -616,6 +658,12 @@ func getAvailableToolsets() []toolsetInfo {
616658
return defaultValue
617659
}
618660

661+
// Initialize tokenizer for token counting
662+
enc, err := tokenizer.Get(tokenizer.Cl100kBase)
663+
if err != nil {
664+
enc = nil // Continue without tokenizer if it fails
665+
}
666+
619667
// Create a dummy toolset group to extract the structure
620668
// We use read-only false to get all tools
621669
tsg := github.DefaultToolsetGroup(false, getClient, getGQLClient, getRawClient, translator, 5000)
@@ -632,12 +680,19 @@ func getAvailableToolsets() []toolsetInfo {
632680
// Get all available tools (both read and write)
633681
allTools := toolset.GetAvailableTools()
634682
for _, tool := range allTools {
635-
ts.tools = append(ts.tools, toolInfo{
683+
toolInfo := toolInfo{
636684
name: tool.Tool.Name,
637685
description: tool.Tool.Description,
638686
toolsetName: toolsetName,
639687
isReadOnly: tool.Tool.Annotations.ReadOnlyHint != nil && *tool.Tool.Annotations.ReadOnlyHint,
640-
})
688+
}
689+
690+
// Estimate token count for this tool using the actual MCP tool
691+
if enc != nil {
692+
toolInfo.tokenCount = estimateToolTokens(enc, tool)
693+
}
694+
695+
ts.tools = append(ts.tools, toolInfo)
641696
}
642697

643698
// Sort tools by name
@@ -670,6 +725,34 @@ func getFirstSentence(description string) string {
670725
return description
671726
}
672727

728+
// estimateToolTokens estimates the token count for a tool's MCP definition
729+
// This serializes the complete tool definition (name, description, annotations, inputSchema)
730+
// to approximate the token count that will be sent to the LLM in the tools/list response
731+
func estimateToolTokens(enc tokenizer.Codec, mcpTool server.ServerTool) int {
732+
if enc == nil {
733+
return 0
734+
}
735+
736+
// Serialize the full MCP tool definition to JSON
737+
// This is what gets sent to the LLM in the tools/list response
738+
jsonBytes, err := json.Marshal(mcpTool.Tool)
739+
if err != nil {
740+
return 0
741+
}
742+
743+
// Encode and count tokens
744+
tokens, _, _ := enc.Encode(string(jsonBytes))
745+
return len(tokens)
746+
}
747+
748+
// formatTokenCount formats token count with K suffix for thousands
749+
func formatTokenCount(count int) string {
750+
if count >= 1000 {
751+
return fmt.Sprintf("%.1fK", float64(count)/1000)
752+
}
753+
return fmt.Sprintf("%d", count)
754+
}
755+
673756

674757
func runConfigure(cmd *cobra.Command, args []string) error {
675758
// Dynamically get available toolsets

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ require (
2424
github.com/charmbracelet/x/ansi v0.10.1 // indirect
2525
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
2626
github.com/charmbracelet/x/term v0.2.1 // indirect
27+
github.com/dlclark/regexp2 v1.11.5 // indirect
2728
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
2829
github.com/go-openapi/jsonpointer v0.19.5 // indirect
2930
github.com/go-openapi/swag v0.21.1 // indirect
@@ -38,6 +39,7 @@ require (
3839
github.com/muesli/cancelreader v0.2.2 // indirect
3940
github.com/muesli/termenv v0.16.0 // indirect
4041
github.com/rivo/uniseg v0.4.7 // indirect
42+
github.com/tiktoken-go/tokenizer v0.7.0 // indirect
4143
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
4244
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
4345
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
2222
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2323
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
2424
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
25+
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
26+
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
2527
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
2628
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
2729
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -124,6 +126,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
124126
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
125127
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
126128
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
129+
github.com/tiktoken-go/tokenizer v0.7.0 h1:VMu6MPT0bXFDHr7UPh9uii7CNItVt3X9K90omxL54vw=
130+
github.com/tiktoken-go/tokenizer v0.7.0/go.mod h1:6UCYI/DtOallbmL7sSy30p6YQv60qNyU/4aVigPOx6w=
127131
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
128132
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
129133
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=

0 commit comments

Comments
 (0)