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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0
0.1.3
3 changes: 2 additions & 1 deletion go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 16 additions & 33 deletions mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,59 @@ 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"
)

// 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://<addr>/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")),
), s.handleScanDir)
}

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
}
Expand All @@ -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
}
Expand All @@ -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)
}
63 changes: 4 additions & 59 deletions mcp/server_more_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -424,70 +424,15 @@ 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) {
s := New()
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 {
Expand Down Expand Up @@ -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")
}
Expand Down
6 changes: 3 additions & 3 deletions mcp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
}

Expand Down
Loading