diff --git a/CHANGELOG.md b/CHANGELOG.md index 3463397..62a46c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm --- -## [Unreleased] +## [0.1.3] - 2026-07-04 ### Changed +- **MCP server scaffolding moved to the shared + [`hawk-mcpkit`](https://github.com/GrayCodeAI/hawk-mcpkit) module.** + `mcp/server.go` now delegates server construction, the stdio and + streamable-HTTP transports, and argument/result helpers to the kit. + Tool names, schemas, and behavior are unchanged. The advertised MCP + server version now tracks the `VERSION` file (`inspect.Version`) + instead of a hardcoded string. - **Version re-baselined to `0.1.0`** in `internal/report/sarif.go` (`const inspectVersion`, used as `tool.driver.version` in SARIF output) and `mcp/server.go` diff --git a/VERSION b/VERSION index 6e8bf73..b1e80bb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 +0.1.3 diff --git a/go.mod b/go.mod index 0b08e07..cf7a694 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module github.com/GrayCodeAI/inspect go 1.26.4 require ( - github.com/GrayCodeAI/hawk-core-contracts v0.1.0 + github.com/GrayCodeAI/hawk-core-contracts v0.1.2 + github.com/GrayCodeAI/hawk-mcpkit v0.1.0 github.com/mark3labs/mcp-go v0.49.0 golang.org/x/net v0.55.0 golang.org/x/time v0.15.0 diff --git a/go.sum b/go.sum index c7c4763..87ad9a3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ -github.com/GrayCodeAI/hawk-core-contracts v0.1.0 h1:w4Y6jBFzjXJ8zmAtfCWuKtODlHKosn9TStADxHE/LsU= -github.com/GrayCodeAI/hawk-core-contracts v0.1.0/go.mod h1:Oq7h+CGkmI3HFTHBVGaWHQuhvtGt6e8UYz5zwF4cM2Q= +github.com/GrayCodeAI/hawk-core-contracts v0.1.2 h1:2N6mc6fwIx1zKAi9nD6GoVF6EQ+b3flSrjM+Biioyxo= +github.com/GrayCodeAI/hawk-core-contracts v0.1.2/go.mod h1:J+I2YYTS3hDwf01bmo1ejbMzOTdYxGn9rZa8CME0pRM= +github.com/GrayCodeAI/hawk-mcpkit v0.1.0 h1:CXJ7V80eHgrDJ3fytE0Nx60RM5VgKe2YF+c3KcjD+ws= +github.com/GrayCodeAI/hawk-mcpkit v0.1.0/go.mod h1:xQpwS9OZCC8qlvEwFXXOYOWQRbYL/OFSI6/5VGvd0hQ= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= diff --git a/mcp/server.go b/mcp/server.go index d7f1b66..727debb 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -2,13 +2,11 @@ package mcp import ( "context" - "encoding/json" "fmt" - "os" "time" + mcpkit "github.com/GrayCodeAI/hawk-mcpkit" mcplib "github.com/mark3labs/mcp-go/mcp" - mcpserver "github.com/mark3labs/mcp-go/server" "github.com/GrayCodeAI/inspect" ) @@ -16,37 +14,39 @@ import ( // Server wraps the inspect library as an MCP server, exposing website // auditing capabilities to any MCP-compatible agent. type Server struct { - server *mcpserver.MCPServer + kit *mcpkit.Server scanner *inspect.Scanner } // New creates an inspect MCP server with the given scanner options. func New(opts ...inspect.Option) *Server { s := &Server{ + kit: mcpkit.New("inspect", inspect.Version), scanner: inspect.NewScanner(opts...), } - s.server = mcpserver.NewMCPServer( - "inspect", "0.1.0", - mcpserver.WithToolCapabilities(true), - ) s.registerTools() return s } // ServeStdio starts the MCP server on stdin/stdout. func (s *Server) ServeStdio() error { - stdio := mcpserver.NewStdioServer(s.server) - return stdio.Listen(context.Background(), os.Stdin, os.Stdout) + return s.kit.ServeStdio() +} + +// ServeHTTP starts the MCP server on a streamable HTTP endpoint. Clients +// connect to http:///mcp. +func (s *Server) ServeHTTP(addr string) error { + return s.kit.ServeHTTP(addr) } func (s *Server) registerTools() { - s.server.AddTool(mcplib.NewTool( + s.kit.AddTool(mcplib.NewTool( "inspect_scan", mcplib.WithDescription("Scan a website for broken links, security issues, and accessibility problems"), mcplib.WithString("url", mcplib.Required(), mcplib.Description("Target URL to scan")), ), s.handleScan) - s.server.AddTool(mcplib.NewTool( + s.kit.AddTool(mcplib.NewTool( "inspect_scan_dir", mcplib.WithDescription("Scan a local directory of HTML files"), mcplib.WithString("path", mcplib.Required(), mcplib.Description("Local directory path")), @@ -54,7 +54,7 @@ func (s *Server) registerTools() { } func (s *Server) handleScan(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { - url := strArg(req, "url") + url := mcpkit.StrArg(req, "url") if url == "" { return mcplib.NewToolResultError("url is required"), nil } @@ -66,16 +66,11 @@ func (s *Server) handleScan(ctx context.Context, req mcplib.CallToolRequest) (*m if err != nil { return mcplib.NewToolResultError(fmt.Sprintf("scan failed: %v", err)), nil } - - b, err := json.MarshalIndent(report, "", " ") - if err != nil { - return nil, err - } - return mcplib.NewToolResultText(string(b)), nil + return mcpkit.JSONResult(report) } func (s *Server) handleScanDir(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { - path := strArg(req, "path") + path := mcpkit.StrArg(req, "path") if path == "" { return mcplib.NewToolResultError("path is required"), nil } @@ -87,17 +82,5 @@ func (s *Server) handleScanDir(ctx context.Context, req mcplib.CallToolRequest) if err != nil { return mcplib.NewToolResultError(fmt.Sprintf("scan_dir failed: %v", err)), nil } - - b, err := json.MarshalIndent(report, "", " ") - if err != nil { - return nil, err - } - return mcplib.NewToolResultText(string(b)), nil -} - -func strArg(req mcplib.CallToolRequest, key string) string { - if v, ok := req.GetArguments()[key].(string); ok { - return v - } - return "" + return mcpkit.JSONResult(report) } diff --git a/mcp/server_more_test.go b/mcp/server_more_test.go index d7a69ec..aef6395 100644 --- a/mcp/server_more_test.go +++ b/mcp/server_more_test.go @@ -130,7 +130,7 @@ func TestErrorHandling_WrongArgumentType(t *testing.T) { if result == nil { t.Fatal("expected result") } - // The int won't match the string type assertion, so strArg returns "". + // The int won't match the string type assertion, so mcpkit.StrArg returns "". if !result.IsError { t.Fatal("expected error result when url is not a string") } @@ -424,62 +424,7 @@ func TestTimeout_WithTimeoutOption(t *testing.T) { } // --------------------------------------------------------------------------- -// 7. strArg helper -// --------------------------------------------------------------------------- - -func TestStrArg(t *testing.T) { - tests := []struct { - name string - args map[string]any - key string - want string - }{ - { - name: "present string", - args: map[string]any{"url": "http://example.com"}, - key: "url", - want: "http://example.com", - }, - { - name: "missing key", - args: map[string]any{}, - key: "url", - want: "", - }, - { - name: "nil arguments", - args: nil, - key: "url", - want: "", - }, - { - name: "wrong type", - args: map[string]any{"url": 42}, - key: "url", - want: "", - }, - { - name: "empty string", - args: map[string]any{"url": ""}, - key: "url", - want: "", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - req := mcplib.CallToolRequest{} - req.Params.Arguments = tc.args - got := strArg(req, tc.key) - if got != tc.want { - t.Errorf("strArg(%q) = %q, want %q", tc.key, got, tc.want) - } - }) - } -} - -// --------------------------------------------------------------------------- -// 8. Server construction +// 7. Server construction // --------------------------------------------------------------------------- func TestNew_ReturnsServer(t *testing.T) { @@ -487,7 +432,7 @@ func TestNew_ReturnsServer(t *testing.T) { if s == nil { t.Fatal("New() returned nil") } - if s.server == nil { + if s.kit == nil || s.kit.MCP() == nil { t.Fatal("internal MCPServer is nil") } if s.scanner == nil { @@ -618,7 +563,7 @@ func TestRegisterTools_InspectScanSchema(t *testing.T) { s := New(inspect.Quick, inspect.WithAllowPrivateIPs()) // Access the underlying MCPServer to verify tool registration. - mcpSrv := s.server + mcpSrv := s.kit.MCP() if mcpSrv == nil { t.Fatal("internal MCPServer is nil") } diff --git a/mcp/server_test.go b/mcp/server_test.go index f2b2d74..1ce3df2 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -93,7 +93,7 @@ func initAndSession(t *testing.T, ts *httptest.Server) string { func newTestHTTPServer(t *testing.T, opts ...inspect.Option) (*httptest.Server, *Server) { t.Helper() s := New(opts...) - ts := mcpserver.NewTestStreamableHTTPServer(s.server, mcpserver.WithStateful(true)) + ts := mcpserver.NewTestStreamableHTTPServer(s.kit.MCP(), mcpserver.WithStateful(true)) t.Cleanup(ts.Close) return ts, s } @@ -149,8 +149,8 @@ func TestProtocol_Initialize(t *testing.T) { if result.Result.ServerInfo.Name != "inspect" { t.Errorf("server name: want inspect, got %s", result.Result.ServerInfo.Name) } - if result.Result.ServerInfo.Version != "0.1.0" { - t.Errorf("server version: want 0.1.0, got %s", result.Result.ServerInfo.Version) + if result.Result.ServerInfo.Version != inspect.Version { + t.Errorf("server version: want %s, got %s", inspect.Version, result.Result.ServerInfo.Version) } }