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
5 changes: 2 additions & 3 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
# CodeRabbit Configuration File
# Reference: https://docs.coderabbit.ai/spec/configuration
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

language: "en-US"
tone_instruction: "Review code as a senior Go software engineer with a focus on concurrency safety, modular monolithic design boundaries, and clear error propagation."

reviews:
profile: "assertive"
tone_instructions: "Review code as a senior Go software engineer with a focus on concurrency safety, modular monolithic design boundaries, and clear error propagation."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

CodeRabbit configuration schema tone_instructions field documentation

💡 Result:

The tone_instructions field in the CodeRabbit configuration schema is used to customize the persona, style, and communication approach of CodeRabbit's review comments and chat responses [1][2][3][4]. Key details about the tone_instructions field: - Schema Type: It is defined as a string [2][3]. - Default Value: It defaults to an empty string (""), which results in CodeRabbit using its standard, default tone [1][2][3]. - Character Limit: The field has a maximum length of 250 characters [1][2]. - Functionality: You can provide natural language prompts to steer how CodeRabbit interacts [3][4]. For example, you can instruct it to be direct, adopt a specific persona (e.g., a nature documentary narrator or an energetic Scrum Master), or focus on specific communication styles like explaining the 'why' behind suggestions [3][4]. - Configuration Method: It is typically configured within the.coderabbit.yaml file located in the root of your repository [5][4]. It can also be set via the CodeRabbit Web UI under Settings > General > Tone Instructions [3]. While tone_instructions controls the overall communication style, it is distinct from other configuration fields such as reviews.instructions (which sets global review rules and conventions) and reviews.path_instructions (which applies context-aware guidance to specific file paths) [4][6][7]. [1][5][8]

Citations:


Fix/confirm tone_instructions placement in .coderabbit.yaml (line 9)

tone_instructions is a top-level string used to customize CodeRabbit’s overall review/chat tone; reviews.* keys are separate. If tone_instructions is nested under reviews, CodeRabbit may ignore it—keep it at the root and use the exact key name tone_instructions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.coderabbit.yaml at line 9, Move the "tone_instructions" entry out of any
nested map (e.g., reviews) and place it as a top-level key named exactly
"tone_instructions" in .coderabbit.yaml so CodeRabbit can read it; verify there
is only one top-level "tone_instructions" string (not under "reviews" or another
section), and keep the value as the desired review tone.

auto_review:
enabled: true
drafts: false
high_level_summary: true
reviews_per_file: true
collapse_dependency_reviews: true

chat:
auto_reply: true
75 changes: 58 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,64 @@ on:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
env:
GOPRIVATE: github.com/GoHyperrr/*
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- name: Checkout hyperrr
uses: actions/checkout@v4
with:
go-version: '1.25'
- run: go mod download
- run: go run ./cmd/builder
- run: go vet ./...
- run: go test -v -coverprofile=coverage.out ./...
- name: Go Coverage Report
uses: ncruces/go-coverage-report@v0
path: hyperrr

- name: Checkout mdk
uses: actions/checkout@v4
with:
repository: GoHyperrr/mdk
ref: ${{ github.head_ref || github.ref_name }}
path: mdk
Comment on lines +22 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cross-repo branch coupling can make PR CI fail non-deterministically.

Using ref: ${{ github.head_ref || github.ref_name }} for mdk/auth/commerce/file-storage assumes the same branch exists in every repository. For most feature branches, checkout will fail before vet/test/build. Use a stable fallback (e.g., default branch) or make per-repo refs explicit.

Also applies to: 29-30, 36-37, 43-44

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 18-23: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 22 - 23, The workflow currently uses a
cross-repo branch expression "ref: ${{ github.head_ref || github.ref_name }}"
(seen next to "path: mdk") which assumes matching branches exist in dependent
repos; change these checkout actions to use a stable fallback or explicit
per-repo ref instead — e.g., replace the expression with a conditional that
falls back to the repository default branch (or a repo-specific input/variable)
for the mdk checkout, and apply the same change to the other occurrences (the
other "ref: ${{ github.head_ref || github.ref_name }}" lines) so CI no longer
fails when feature branches aren't present in every repo.


- name: Checkout auth
uses: actions/checkout@v4
with:
repository: GoHyperrr/auth
ref: ${{ github.head_ref || github.ref_name }}
path: auth

- name: Checkout commerce
uses: actions/checkout@v4
with:
repository: GoHyperrr/commerce
ref: ${{ github.head_ref || github.ref_name }}
path: commerce

- name: Checkout file-storage
uses: actions/checkout@v4
with:
report: 'true'
chart: 'true'
amend: 'true'
continue-on-error: true
repository: GoHyperrr/file-storage
ref: ${{ github.head_ref || github.ref_name }}
path: file-storage

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
Comment on lines +13 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Pin GitHub Actions to commit SHAs and disable credential persistence.

The workflow has two security posture gaps:

  1. Unpinned actions (lines 14, 19, 25, 31, 37, 43): Using tag references like @v4 and @v5 instead of commit SHAs allows maintainers to retroactively modify the action code, creating supply-chain risk.

  2. Missing persist-credentials: false in checkout steps: The default behavior persists the GitHub token in .git/config, making it available to subsequent steps and any scripts they execute.

🔒 Proposed fix

Pin actions to their commit hashes and disable credential persistence:

       - name: Checkout hyperrr
-        uses: actions/checkout@v4
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           path: hyperrr
+          persist-credentials: false

       - name: Checkout mdk
-        uses: actions/checkout@v4
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           repository: GoHyperrr/mdk
           path: mdk
+          persist-credentials: false

       - name: Checkout auth
-        uses: actions/checkout@v4
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           repository: GoHyperrr/auth
           path: auth
+          persist-credentials: false

       - name: Checkout commerce
-        uses: actions/checkout@v4
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           repository: GoHyperrr/commerce
           path: commerce
+          persist-credentials: false

       - name: Checkout file-storage
-        uses: actions/checkout@v4
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           repository: GoHyperrr/file-storage
           path: file-storage
+          persist-credentials: false

       - name: Setup Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
         with:

Note: Verify the commit SHAs above correspond to the desired versions before applying.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Checkout hyperrr
uses: actions/checkout@v4
with:
go-version: '1.25'
- run: go mod download
- run: go run ./cmd/builder
- run: go vet ./...
- run: go test -v -coverprofile=coverage.out ./...
- name: Go Coverage Report
uses: ncruces/go-coverage-report@v0
path: hyperrr
- name: Checkout mdk
uses: actions/checkout@v4
with:
repository: GoHyperrr/mdk
path: mdk
- name: Checkout auth
uses: actions/checkout@v4
with:
repository: GoHyperrr/auth
path: auth
- name: Checkout commerce
uses: actions/checkout@v4
with:
repository: GoHyperrr/commerce
path: commerce
- name: Checkout file-storage
uses: actions/checkout@v4
with:
report: 'true'
chart: 'true'
amend: 'true'
continue-on-error: true
repository: GoHyperrr/file-storage
path: file-storage
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Checkout hyperrr
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
path: hyperrr
persist-credentials: false
- name: Checkout mdk
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
repository: GoHyperrr/mdk
path: mdk
persist-credentials: false
- name: Checkout auth
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
repository: GoHyperrr/auth
path: auth
persist-credentials: false
- name: Checkout commerce
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
repository: GoHyperrr/commerce
path: commerce
persist-credentials: false
- name: Checkout file-storage
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
repository: GoHyperrr/file-storage
path: file-storage
persist-credentials: false
- name: Setup Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: '1.25'
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 13-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 18-22: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 24-28: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 30-34: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 36-40: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 14-14: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 19-19: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 25-25: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 31-31: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 37-37: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 43-43: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 13 - 45, Replace all tag references
for actions (the uses: entries for actions/checkout@v4 and actions/setup-go@v5)
with their specific commit SHAs and add persist-credentials: false to every
checkout step; update the steps named "Checkout hyperrr", "Checkout mdk",
"Checkout auth", "Checkout commerce", and "Checkout file-storage" to include
persist-credentials: false under their with: blocks and change uses:
actions/checkout@v4 to uses: actions/checkout@<COMMIT_SHA>, and change "Setup
Go" uses: actions/setup-go@v5 to uses: actions/setup-go@<COMMIT_SHA> (replace
<COMMIT_SHA> with the verified commit hashes).

Source: Linters/SAST tools


- name: Setup Workspace
run: |
echo "go 1.25.5" > go.work
echo "use (" >> go.work
echo " ./auth" >> go.work
echo " ./commerce" >> go.work
echo " ./file-storage" >> go.work
echo " ./hyperrr" >> go.work
echo " ./mdk" >> go.work
echo ")" >> go.work
Comment on lines +51 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Invalid go.work version directive.

Line 49 specifies go 1.25.5, which has two problems:

  1. Go 1.25 does not exist (see previous comment)
  2. The go directive in go.work files requires major.minor format (e.g., go 1.23), not major.minor.patch
🔧 Proposed fix

Once the correct Go version is confirmed, update line 49:

-          echo "go 1.25.5" > go.work
+          echo "go 1.23" > go.work

(Replace 1.23 with the actual target version in major.minor format.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Setup Workspace
run: |
echo "go 1.25.5" > go.work
echo "use (" >> go.work
echo " ./auth" >> go.work
echo " ./commerce" >> go.work
echo " ./file-storage" >> go.work
echo " ./hyperrr" >> go.work
echo " ./mdk" >> go.work
echo ")" >> go.work
- name: Setup Workspace
run: |
echo "go 1.23" > go.work
echo "use (" >> go.work
echo " ./auth" >> go.work
echo " ./commerce" >> go.work
echo " ./file-storage" >> go.work
echo " ./hyperrr" >> go.work
echo " ./mdk" >> go.work
echo ")" >> go.work
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 47 - 56, The go.work generation writes
an invalid version directive "go 1.25.5"; update the version line emitted by the
CI step that writes go.work so it uses a valid major.minor Go directive (e.g.,
"go 1.23" or whatever the project's supported Go version is) instead of a
patch-level value, ensuring the echoed line that currently outputs `go 1.25.5`
is replaced with the correct `go X.Y` string.


- name: Generate Code
run: cd hyperrr && go run build_ci.go

- name: Go Vet
run: cd hyperrr && go vet ./...

- name: Go Test
run: cd hyperrr && go test -v ./...

- name: Go Build
run: cd hyperrr && go build ./cmd/hyperrr
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Changelog

All notable changes to this project will be documented in this file.

## [0.1.0] - 2026-06-05

### Added
- Multi-repo local workspace testing CI workflows.
- Lightweight built-in local disk and memory storage provider.
- Static plugin Cobra CLI registrations and string-based step resolutions.
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,16 @@ Hyperrr is a modern, event-native commerce engine designed as a modular monolith

## 🚥 Getting Started

### 1. Compile the Unified Executable
Compile the consolidated binary using Go:
### 1. Run Schema Aggregation & Compile the Executable
Discover GraphQL schemas, run `gqlgen` code generation, and compile the unified `hyperrr` server executable:
```bash
# Using standard Go
go build -o bin/hyperrr ./cmd/hyperrr
go run ./cmd/builder
```
This writes the compiled binary to `bin/hyperrr` (or `bin/hyperrr.exe` on Windows).

# Or using Makefile
make build
After compiling, you can also use the CLI's built-in build command to rebuild dynamically:
```bash
./bin/hyperrr build
```

### 2. Start the Backend Commerce Server
Expand Down Expand Up @@ -121,10 +123,10 @@ You can register users via GraphQL or directly from your terminal using the dyna

```bash
# Register a user via the CLI
go run ./cmd/hyperrr auth user register dev@example.com mypassword "Developer User"
./bin/hyperrr auth user register dev@example.com mypassword "Developer User"

# Generate an API Key for MCP AI Agents
go run ./cmd/hyperrr auth apikey generate
./bin/hyperrr auth apikey generate
```

To register via the GraphQL Playground (which emits `identity.user_created` to automatically create a customer profile):
Expand Down
11 changes: 11 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Security Policy

## Supported Versions

For pre-1.0 releases we support the latest release or the latest patch series (e.g., 0.1.x), and for 1.0+ we support the latest major release branch.

## Reporting a Vulnerability

If you discover a security vulnerability within Hyperrr or any of its modules, please do not disclose it publicly. Report it directly by emailing security@hyperrr.org or opening a draft security advisory on GitHub.

We will acknowledge receipt of your report within 48 hours and work with you to patch the issue promptly.
2 changes: 2 additions & 0 deletions api/gqlgen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ model:

autobind:
- github.com/GoHyperrr/commerce/product
- github.com/GoHyperrr/commerce/seo
- github.com/GoHyperrr/commerce/taxonomy
- github.com/GoHyperrr/commerce/cart
- github.com/GoHyperrr/commerce/order
- github.com/GoHyperrr/commerce/customer
Expand Down
31 changes: 24 additions & 7 deletions api/graph/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,14 @@ func TestResolvers(t *testing.T) {

t.Run("Product Resolvers", func(t *testing.T) {
// Create a product
p := &product.Product{ID: "p1", Name: "Product 1", Price: 10.0}
p := &product.Product{
ID: "p1",
Name: "Product 1",
Handle: "product-1",
Variants: []product.ProductVariant{
{ID: "v1", Title: "Default", Price: 10.0},
},
}
prodMod.Repo().Save(ctx, p)

res, err := resolver.Query().GetProduct(ctx, "p1")
Expand All @@ -176,9 +183,12 @@ func TestResolvers(t *testing.T) {
t.Run("Product Mutations", func(t *testing.T) {
// Create
createInput := product.CreateProductInput{
ID: "p_new",
Name: "New Product",
Price: 50.0,
ID: "p_new",
Name: "New Product",
Handle: "new-product",
Variants: []product.CreateProductVariantInput{
{Title: "Default", Price: 50.0},
},
}
res, err := resolver.Mutation().CreateProduct(ctx, createInput)
if err != nil || res.Name != "New Product" {
Expand All @@ -199,8 +209,8 @@ func TestResolvers(t *testing.T) {
t.Fatalf("DeleteProduct failed: %v", err)
}

// Create failure (missing name)
_, err = resolver.Mutation().CreateProduct(ctx, product.CreateProductInput{ID: "fail", Price: 10.0})
// Create failure (missing required fields: name and handle)
_, err = resolver.Mutation().CreateProduct(ctx, product.CreateProductInput{ID: "fail", Handle: ""})
if err == nil {
t.Error("expected error for invalid product create")
}
Expand Down Expand Up @@ -742,7 +752,14 @@ func TestResolvers(t *testing.T) {
})
badResolver3 := *resolver
badResolver3.ProductModule = badProductMod3
_, err = badResolver3.Mutation().CreateProduct(ctx, product.CreateProductInput{ID: "p_fail", Name: "Fail", Price: 10})
_, err = badResolver3.Mutation().CreateProduct(ctx, product.CreateProductInput{
ID: "p_fail",
Name: "Fail",
Handle: "fail",
Variants: []product.CreateProductVariantInput{
{Title: "Default", Price: 10.0},
},
})
if err == nil || !strings.Contains(err.Error(), "failed to retrieve created product") {
t.Errorf("expected 'failed to retrieve created product' error, got %v", err)
}
Expand Down
8 changes: 4 additions & 4 deletions api/mcp/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (m *mockWorkflowRegistry) List() []*workflow.Workflow {
type mockWorkflowRunner struct {
lastExecuted string
lastInput any
lastActor *ident.Actor
lastActor ident.Actor
}

func (m *mockWorkflowRunner) ExecuteSyncWorkflow(ctx context.Context, id string, wf *workflow.Workflow, input any) (map[string]any, error) {
Expand Down Expand Up @@ -88,7 +88,7 @@ func TestMCP_DiscoveryAndExecution(t *testing.T) {
})

t.Run("Tools Call Execution", func(t *testing.T) {
actor := &ident.Actor{ID: "agent_1", Type: ident.ActorAIAgent}
actor := &ident.BaseActor{ID: "agent_1", Type: ident.ActorAIAgent}
params := map[string]any{
"name": "public-tool",
"arguments": map[string]any{"id": "123"},
Expand All @@ -108,7 +108,7 @@ func TestMCP_DiscoveryAndExecution(t *testing.T) {
t.Error("runner was not invoked with the correct tool")
}

if runner.lastActor == nil || runner.lastActor.ID != "agent_1" {
if runner.lastActor == nil || runner.lastActor.GetID() != "agent_1" {
t.Errorf("expected actor ID agent_1, got %v", runner.lastActor)
}

Expand All @@ -119,7 +119,7 @@ func TestMCP_DiscoveryAndExecution(t *testing.T) {
})

t.Run("Tools Call Unauthorized Tool", func(t *testing.T) {
actor := &ident.Actor{ID: "agent_1", Type: ident.ActorAIAgent}
actor := &ident.BaseActor{ID: "agent_1", Type: ident.ActorAIAgent}
params := map[string]any{"name": "private-tool"}

_, errRPC := server.handleToolsCall(context.Background(), actor, params)
Expand Down
60 changes: 53 additions & 7 deletions api/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,15 @@ func (s *Server) HandleMessages(w http.ResponseWriter, r *http.Request) {
providers = []string{"apikey"} // Default fallback
}

var actor *ident.Actor
var actor ident.Actor
var authErr error
authenticated := false

for _, p := range providers {
switch p {
case "none":
// Bypass authentication check entirely and mock a developer actor
actor = &ident.Actor{
actor = &ident.BaseActor{
ID: "act_mcp_developer",
Type: ident.ActorAIAgent,
Name: "Developer Agent (No Auth)",
Expand All @@ -175,7 +175,7 @@ func (s *Server) HandleMessages(w http.ResponseWriter, r *http.Request) {
authErr = fmt.Errorf("invalid api key: %w", err)
continue
}
if resActor.Type != ident.ActorAIAgent {
if resActor.GetType() != ident.ActorAIAgent {
authErr = fmt.Errorf("actor is not an AI agent")
continue
}
Expand Down Expand Up @@ -214,7 +214,7 @@ func (s *Server) HandleMessages(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusAccepted)
}

func (s *Server) dispatch(ctx context.Context, sessionID string, actor *ident.Actor, req JSONRPCRequest) {
func (s *Server) dispatch(ctx context.Context, sessionID string, actor ident.Actor, req JSONRPCRequest) {
var resp JSONRPCResponse
resp.JSONRPC = "2.0"
resp.ID = req.ID
Expand Down Expand Up @@ -483,7 +483,7 @@ func (s *Server) handleToolsList(ctx context.Context) *ListToolsResult {
return &ListToolsResult{Tools: tools}
}

func (s *Server) handleToolsCall(ctx context.Context, actor *ident.Actor, params map[string]any) (any, *Error) {
func (s *Server) handleToolsCall(ctx context.Context, actor ident.Actor, params map[string]any) (any, *Error) {
name, ok := params["name"].(string)
if !ok {
return nil, &Error{Code: CodeInvalidParams, Message: "Tool name required"}
Expand Down Expand Up @@ -925,14 +925,43 @@ func (s *Server) renderUI(ctx context.Context, appName string) string {
content += `<tr><td colspan="5" style="text-align: center; color: var(--text-secondary);">No products registered.</td></tr>`
} else {
for _, p := range list {
currencyCode := "USD"
if s.deps.Config != nil && s.deps.Config.Currency != "" {
currencyCode = s.deps.Config.Currency
}
if p.Metadata != nil {
if curr, ok := p.Metadata["currency"].(string); ok {
currencyCode = curr
}
}
Comment on lines +928 to +936

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider validating non-empty currency from metadata.

If p.Metadata["currency"] contains an empty string, the type assertion succeeds and currencyCode becomes empty, causing formatPrice to produce output like "123.45 " with a trailing space.

🔧 Proposed fix
 if p.Metadata != nil {
-    if curr, ok := p.Metadata["currency"].(string); ok {
+    if curr, ok := p.Metadata["currency"].(string); ok && curr != "" {
         currencyCode = curr
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/mcp/server.go` around lines 928 - 936, The code assigns
p.Metadata["currency"] to currencyCode even when it's an empty string, causing
formatPrice output with a trailing space; update the assignment in the block
that checks p.Metadata (around the currencyCode variable and the
p.Metadata["currency"] type assertion) to only set currencyCode when the
asserted string is non-empty (e.g., check curr != "" or strings.TrimSpace(curr)
!= "") so that empty metadata values fallback to the Config/default USD instead
of overwriting with an empty string.


priceStr := "N/A"
if len(p.Variants) > 0 {
priceStr = formatPrice(p.Variants[0].Price, currencyCode)
if len(p.Variants) > 1 {
minP := p.Variants[0].Price
maxP := p.Variants[0].Price
for _, v := range p.Variants {
if v.Price < minP {
minP = v.Price
}
if v.Price > maxP {
maxP = v.Price
}
}
if minP != maxP {
priceStr = fmt.Sprintf("%s - %s", formatPrice(minP, currencyCode), formatPrice(maxP, currencyCode))
}
}
}
content += fmt.Sprintf(`
<tr>
<td><code>%s</code></td>
<td>%s</td>
<td>%s</td>
<td class="text-accent">$%.2f</td>
<td class="text-accent">%s</td>
<td>%s</td>
</tr>`, p.ID, p.Name, p.Description, p.Price, p.Currency)
</tr>`, p.ID, p.Name, p.Description, priceStr, currencyCode)
}
}
content += `
Expand Down Expand Up @@ -1842,3 +1871,20 @@ func (s *Server) renderUI(ctx context.Context, appName string) string {

return fmt.Sprintf(htmlSkeleton, title, accent, accentGlow, title, content)
}

func formatPrice(price float64, currencyCode string) string {
switch strings.ToUpper(currencyCode) {
case "USD":
return fmt.Sprintf("$%.2f", price)
case "EUR":
return fmt.Sprintf("€%.2f", price)
case "GBP":
return fmt.Sprintf("£%.2f", price)
case "JPY":
return fmt.Sprintf("¥%.0f", price)
case "INR":
return fmt.Sprintf("₹%.2f", price)
default:
return fmt.Sprintf("%.2f %s", price, currencyCode)
}
}
Loading
Loading