Skip to content

Commit bd756c8

Browse files
authored
Merge pull request #119 from IBM/add-mcp-server-time-go
Added fast MCP server in go
2 parents 17bb99c + 9d7553e commit bd756c8

File tree

10 files changed

+344
-4
lines changed

10 files changed

+344
-4
lines changed

.github/tools/cleanup.sh

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/env bash
2+
#───────────────────────────────────────────────────────────────────────────────
3+
# Script : cleanup.sh
4+
# Author : Mihai Criveti
5+
# Purpose: Prune old or unused GHCR container versions for IBM's MCP Context Forge
6+
# Copyright 2025
7+
# SPDX-License-Identifier: Apache-2.0
8+
#
9+
# Description:
10+
# This script safely manages container versions in GitHub Container Registry
11+
# (ghcr.io) under the IBM organization, specifically targeting the
12+
# `mcp-context-forge` package. It supports interactive and non-interactive
13+
# deletion modes to help you keep the container registry clean.
14+
#
15+
# Features:
16+
# • Dry-run by default to avoid accidental deletion
17+
# • Tag whitelisting with regular expression matching
18+
# • GitHub CLI integration with scope validation
19+
# • CI/CD-compatible via environment overrides
20+
#
21+
# Requirements:
22+
# • GitHub CLI (gh) v2.x with appropriate scopes
23+
# • jq (command-line JSON processor)
24+
#
25+
# Required Token Scopes:
26+
# delete:packages
27+
#
28+
# Authentication Notes:
29+
# Authenticate with:
30+
# gh auth refresh -h github.com -s read:packages,delete:packages
31+
# Or:
32+
# gh auth logout
33+
# gh auth login --scopes "read:packages,delete:packages,write:packages,repo,read:org,gist"
34+
#
35+
# Verify authentication with:
36+
# gh auth status -t
37+
#
38+
# Environment Variables:
39+
# GITHUB_TOKEN / GH_TOKEN : GitHub token with required scopes
40+
# DRY_RUN : Set to "false" to enable actual deletions (default: true)
41+
#
42+
# Usage:
43+
# ./cleanup.sh # Dry-run with confirmation prompt
44+
# DRY_RUN=false ./cleanup.sh --yes # Actual deletion without prompt (for CI)
45+
#
46+
#───────────────────────────────────────────────────────────────────────────────
47+
48+
set -euo pipefail
49+
50+
##############################################################################
51+
# 1. PICK A TOKEN
52+
##############################################################################
53+
NEEDED_SCOPES="delete:packages"
54+
55+
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
56+
TOKEN="$GITHUB_TOKEN"
57+
elif [[ -n "${GH_TOKEN:-}" ]]; then
58+
TOKEN="$GH_TOKEN"
59+
else
60+
# fall back to whatever gh already has
61+
if ! TOKEN=$(gh auth token 2>/dev/null); then
62+
echo "❌ No token exported and gh not logged in. Fix with:"
63+
echo " gh auth login (or export GITHUB_TOKEN)"
64+
exit 1
65+
fi
66+
fi
67+
export GH_TOKEN="$TOKEN" # gh api uses this
68+
69+
# Fixed scope checking - check for both required scopes individually
70+
if scopes=$(gh auth status --show-token 2>/dev/null | grep -oP 'Token scopes: \K.*' || echo ""); then
71+
missing_scopes=()
72+
73+
# if ! echo "$scopes" | grep -q "read:packages"; then
74+
# missing_scopes+=("read:packages")
75+
# fi
76+
77+
if ! echo "$scopes" | grep -q "delete:packages"; then
78+
missing_scopes+=("delete:packages")
79+
fi
80+
81+
if [[ ${#missing_scopes[@]} -gt 0 ]]; then
82+
echo "⚠️ Your token scopes are [$scopes] – but you're missing: [$(IFS=','; echo "${missing_scopes[*]}")]"
83+
echo " Run: gh auth refresh -h github.com -s $NEEDED_SCOPES"
84+
exit 1
85+
fi
86+
else
87+
echo "⚠️ Could not verify token scopes. Proceeding anyway..."
88+
fi
89+
90+
##############################################################################
91+
# 2. CONFIG
92+
##############################################################################
93+
ORG="ibm"
94+
PKG="mcp-context-forge"
95+
KEEP_TAGS=( "0.1.0" "v0.1.0" "0.1.1" "v0.1.1" "latest" )
96+
PER_PAGE=100
97+
98+
DRY_RUN=${DRY_RUN:-true} # default safe
99+
ASK_CONFIRM=true
100+
[[ ${1:-} == "--yes" ]] && ASK_CONFIRM=false
101+
KEEP_REGEX="^($(IFS='|'; echo "${KEEP_TAGS[*]}"))$"
102+
103+
##############################################################################
104+
# 3. MAIN
105+
##############################################################################
106+
delete_ids=()
107+
108+
echo "📦 Scanning ghcr.io/${ORG}/${PKG}"
109+
110+
# Process versions and collect IDs to delete
111+
while IFS= read -r row; do
112+
id=$(jq -r '.id' <<<"$row")
113+
digest=$(jq -r '.digest' <<<"$row")
114+
tags_csv=$(jq -r '.tags | join(",")' <<<"$row")
115+
keep=$(jq -e --arg re "$KEEP_REGEX" 'any(.tags[]?; test($re))' <<<"$row" 2>/dev/null) || keep=false
116+
117+
if [[ $keep == true ]]; then
118+
printf "✅ KEEP %s [%s]\n" "$digest" "$tags_csv"
119+
else
120+
printf "🗑️ DELETE %s [%s]\n" "$digest" "$tags_csv"
121+
delete_ids+=("$id")
122+
fi
123+
done < <(gh api -H "Accept: application/vnd.github+json" \
124+
"/orgs/${ORG}/packages/container/${PKG}/versions?per_page=${PER_PAGE}" \
125+
--paginate | \
126+
jq -cr --arg re "$KEEP_REGEX" '
127+
.[] |
128+
{
129+
id,
130+
digest: .metadata.container.digest,
131+
tags: (.metadata.container.tags // [])
132+
}
133+
')
134+
135+
##############################################################################
136+
# 4. CONFIRMATION & DELETION
137+
##############################################################################
138+
if [[ ${#delete_ids[@]} -eq 0 ]]; then
139+
echo "✨ Nothing to delete!"
140+
exit 0
141+
fi
142+
143+
if [[ $DRY_RUN == true ]]; then
144+
if [[ $ASK_CONFIRM == true ]]; then
145+
echo
146+
read -rp "Proceed to delete the ${#delete_ids[@]} versions listed above? (y/N) " reply
147+
[[ $reply =~ ^[Yy]$ ]] || { echo "Aborted – nothing deleted."; exit 0; }
148+
fi
149+
echo "🚀 Re-running in destructive mode …"
150+
DRY_RUN=false exec "$0" --yes
151+
else
152+
echo "🗑️ Deleting ${#delete_ids[@]} versions..."
153+
for id in "${delete_ids[@]}"; do
154+
if gh api -X DELETE -H "Accept: application/vnd.github+json" \
155+
"/orgs/${ORG}/packages/container/${PKG}/versions/${id}" >/dev/null 2>&1; then
156+
echo "✅ Deleted version ID: $id"
157+
else
158+
echo "❌ Failed to delete version ID: $id"
159+
fi
160+
done
161+
echo "Done."
162+
fi

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ ContextForge MCP Gateway is a feature-rich gateway, proxy and MCP Registry that
3131
- [1 · Minimum viable run](#1--minimum-viable-run)
3232
- [2 · Persist the SQLite database](#2--persist-the-sqlite-database)
3333
- [3 · Local tool discovery (host network)](#3--local-tool-discovery-host-network)
34-
- [Podman (rootless-friendly)](#-podman-rootless-friendly)
34+
- [Podman (rootless-friendly)](#-podman-rootless-friendly)
3535
- [1 · Basic run](#1--basic-run)
3636
- [2 · Persist SQLite](#2--persist-sqlite)
3737
- [3 · Host networking (rootless)](#3--host-networking-rootless)

docs/docs/development/index.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,5 +154,3 @@ GitHub Actions enforce:
154154
CI configs live in `.github/workflows/`.
155155

156156
---
157-
158-
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fast-time-server
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# fast-time-server
2+
3+
Ultra‑light MCP server in Go that exposes a single tool, `get_system_time(timezone)`, returning the current time in RFC 3339.
4+
5+
---
6+
7+
## Prerequisites
8+
9+
* **Go ≥ 1.22** ([https://go.dev/dl/](https://go.dev/dl/))
10+
11+
---
12+
13+
## Build
14+
15+
Use the provided `build.sh` helper:
16+
17+
```bash
18+
./build.sh
19+
```
20+
21+
<details>
22+
<summary>What <code>build.sh</code> does</summary>
23+
24+
```bash
25+
#!/usr/bin/env bash
26+
set -euo pipefail
27+
28+
go mod tidy # ensure deps are synced
29+
GOFLAGS="-trimpath -ldflags=-s -w" \
30+
go build -o fast-time-server .
31+
```
32+
33+
</details>
34+
35+
---
36+
37+
## Run
38+
39+
```bash
40+
npx -y supergateway \
41+
--stdio "./fast-time-server" \
42+
--port 8003
43+
```
44+
45+
---
46+
47+
48+
## Benchmark
49+
50+
```
51+
while :; do curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \
52+
-H "Content-Type: application/json" \
53+
-d '{"jsonrpc":"2.0","id":1,"method":"get_system_time"}' \
54+
http://localhost:4444/rpc; done
55+
56+
# or
57+
hey -n 100 -c 10 -m POST \
58+
-H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \
59+
-H "Content-Type: application/json" \
60+
-d '{"jsonrpc":"2.0","id":1,"method":"get_system_time"}' \
61+
http://localhost:4444/rpc
62+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
go mod tidy
3+
go build -trimpath -ldflags "-s -w" -o fast-time-server
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module fast-time-server
2+
3+
go 1.23
4+
5+
toolchain go1.23.10
6+
7+
require github.com/mark3labs/mcp-go v0.32.0 // MCP server/runtime
8+
9+
require (
10+
github.com/google/uuid v1.6.0 // indirect
11+
github.com/spf13/cast v1.7.1 // indirect
12+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
13+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
4+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
5+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
6+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
7+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
8+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
9+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
10+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
11+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
12+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
13+
github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8=
14+
github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
15+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
16+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
17+
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
18+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
19+
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
20+
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
21+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
22+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
23+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
24+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
25+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
26+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// -*- coding: utf-8 -*-
2+
// fast-time-server – ultra‑fast MCP server exposing get_system_time
3+
//
4+
// Copyright 2025
5+
// SPDX-License-Identifier: Apache-2.0
6+
// Authors: Mihai Criveti
7+
//
8+
// This file implements an MCP server written in Go that provides a single
9+
// tool `get_system_time(timezone)` returning the current wall‑clock time
10+
// for a given IANA timezone in RFC3339 format.
11+
//
12+
// It uses the `mcp-go` library for MCP protocol handling and provides
13+
// a simple, efficient implementation that avoids unnecessary parsing of
14+
// timezone data by caching loaded locations.
15+
package main
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"log"
21+
"sync"
22+
"time"
23+
24+
"github.com/mark3labs/mcp-go/mcp"
25+
"github.com/mark3labs/mcp-go/server"
26+
)
27+
28+
// --- ultra-light tz cache (avoids parsing zoneinfo repeatedly)
29+
var tzCache sync.Map
30+
31+
func loadLocation(name string) (*time.Location, error) {
32+
if loc, ok := tzCache.Load(name); ok {
33+
return loc.(*time.Location), nil
34+
}
35+
loc, err := time.LoadLocation(name)
36+
if err != nil {
37+
return nil, err
38+
}
39+
tzCache.Store(name, loc)
40+
return loc, nil
41+
}
42+
43+
func main() {
44+
// Create the core MCP server – no custom logger needed
45+
s := server.NewMCPServer(
46+
"fast-time-server",
47+
"1.0.0",
48+
server.WithToolCapabilities(false), // expose only tools
49+
server.WithLogging(), // use built-in logging
50+
server.WithRecovery(), // panic-safe handlers
51+
)
52+
53+
// Declare the tool schema
54+
timeTool := mcp.NewTool("get_system_time",
55+
mcp.WithDescription("Return current time in RFC3339 for an IANA timezone"),
56+
mcp.WithString("timezone", mcp.Description("IANA zone, default UTC")),
57+
)
58+
59+
// Attach the handler
60+
s.AddTool(timeTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
61+
tz := req.GetString("timezone", "UTC") // modern accessor
62+
63+
loc, err := loadLocation(tz)
64+
if err != nil {
65+
return mcp.NewToolResultError(fmt.Sprintf("unknown timezone %q", tz)), nil
66+
}
67+
now := time.Now().In(loc).Format(time.RFC3339)
68+
return mcp.NewToolResultText(now), nil
69+
})
70+
71+
// Serve over stdio (fastest transport)
72+
if err := server.ServeStdio(s); err != nil {
73+
log.Fatalf("server error: %v", err)
74+
}
75+
}

mcpgateway/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ class MCPPathRewriteMiddleware:
269269
- Only rewrites paths ending with '/mcp' but not exactly '/mcp'.
270270
- Performs authentication before rewriting.
271271
- Passes rewritten requests to `streamable_http_session`.
272-
- All other requests are passed through unchanged.
272+
- All other requests are passed through without change.
273273
"""
274274

275275
def __init__(self, app):

0 commit comments

Comments
 (0)