Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
- name: Vet
run: go vet ./...
- name: Test with race detector
# Force bash so the coverprofile flag parses consistently across the
# OS matrix; PowerShell (the Windows default) mangles -flag=value.ext.
shell: bash
run: go test -race -coverprofile=coverage.out ./...
- name: Build
run: go build ./...
Expand All @@ -38,6 +41,6 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: '1.26'
- uses: golangci/golangci-lint-action@v6
- uses: golangci/golangci-lint-action@v9
with:
version: latest
version: v2.12.2
48 changes: 32 additions & 16 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
version: "2"
run:
timeout: 3m
tests: true

linters:
enable:
- govet
- staticcheck
- errcheck
- ineffassign
- unused
- gofmt
- goimports
- misspell
- unconvert
- revive

- unconvert
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
# Example and command mains print to stdout by design, and test setup
# (mock handlers, fixture writes) intentionally ignores errors.
- linters:
- errcheck
path: (examples|cmd)/|_test\.go
# Handler/test signatures are fixed by the interfaces they satisfy.
- path: _test\.go
text: unused-parameter
paths:
- third_party$
- builtin$
- examples$
issues:
exclude-rules:
# Example and command mains print to stdout by design.
- path: (examples|cmd)/
linters:
- errcheck
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,22 @@ library. It compiles and tests anywhere, with no supply chain to audit.

## Why

Most MCP servers wrap a vendor SDK. mcpkit implements the protocol from the wire
up — the initialize handshake, capability negotiation, tools/resources/prompts,
cancellation and progress — so it doubles as a readable reference for how MCP
actually works, and as a solid base you can extend without pulling in a
dependency tree.
There is an official MCP Go SDK, and for most production servers it's the right
choice — it tracks the spec and is battle-tested across many clients. mcpkit is a
deliberate alternative: it implements the protocol from the wire up — the
initialize handshake, capability negotiation, tools/resources/prompts,
cancellation and progress — on the standard library alone. That buys two things
the SDK route can't:

- **A readable reference** for how MCP actually works, with nothing hidden behind
a dependency. The demux, the session state and the cancellation plumbing are
all here to read.
- **A zero-dependency, auditable base** for security-sensitive tooling. This
server ships an SSRF guard, a sandboxed filesystem and an allowlisted shell —
and there is no third-party supply chain to vet, patch or keep in sync.

Reach for the official SDK when you want to ship and forget; reach for mcpkit when
you want to understand the protocol end to end or extend a base you fully control.

## Highlights

Expand Down
20 changes: 10 additions & 10 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@ type GatewayConfig struct {

// ToolsConfig enables and configures individual tools.
type ToolsConfig struct {
Calc bool `json:"calc"`
Fetch FetchConfig `json:"fetch"`
FS FSConfig `json:"fs"`
Shell ShellConfig `json:"shell"`
KV KVConfig `json:"kv"`
WebSearch WebConfig `json:"webSearch"`
RAG bool `json:"rag"`
Time bool `json:"time"`
Text bool `json:"text"`
JSONQuery bool `json:"jsonQuery"`
Calc bool `json:"calc"`
Fetch FetchConfig `json:"fetch"`
FS FSConfig `json:"fs"`
Shell ShellConfig `json:"shell"`
KV KVConfig `json:"kv"`
WebSearch WebConfig `json:"webSearch"`
RAG bool `json:"rag"`
Time bool `json:"time"`
Text bool `json:"text"`
JSONQuery bool `json:"jsonQuery"`
}

// FetchConfig configures the http_fetch tool.
Expand Down
4 changes: 3 additions & 1 deletion internal/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ func rpcHandler(srv *server.Server, logger *log.Logger) http.HandlerFunc {
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(resp)
if _, err := w.Write(resp); err != nil {
logger.Error("gateway write", "err", err)
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion tools/calc/calc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestEval(t *testing.T) {
{"1 + 2 * 3", 7},
{"(1 + 2) * 3", 9},
{"2 ^ 3 ^ 2", 512}, // right associative
{"-2 ^ 2", -4}, // unary binds looser than power here: -(2^2)
{"-2 ^ 2", -4}, // unary binds looser than power here: -(2^2)
{"10 % 3", 1},
{"sqrt(16)", 4},
{"abs(-5)", 5},
Expand Down
4 changes: 2 additions & 2 deletions tools/calc/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ func (Tool) Definition() mcp.Tool {
"expression": tools.Str("the expression to evaluate, e.g. \"2 * (3 + 4) ^ 2\""),
}, "expression"),
Annotations: &mcp.ToolAnnotations{
Title: "Calculator",
ReadOnlyHint: true,
Title: "Calculator",
ReadOnlyHint: true,
OpenWorldHint: false,
},
}
Expand Down
2 changes: 1 addition & 1 deletion tools/websearch/websearch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestSearch(t *testing.T) {
func TestSearchRespectsLimit(t *testing.T) {
srv := testServer(t)
defer srv.Close()
_, results, _ := NewSearcher(WithEndpoint(srv.URL + "/")).Search(context.Background(), "x", 1)
_, results, _ := NewSearcher(WithEndpoint(srv.URL+"/")).Search(context.Background(), "x", 1)
if len(results) != 1 {
t.Fatalf("limit not applied: %d", len(results))
}
Expand Down
Loading