This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 484d02fb7 fix(rmqctl): keep integer literals in studio payloads and 
name rejected MCP frames (#5262)
484d02fb7 is described below

commit 484d02fb7b8630d75fef30d2a02a6be60c9dd64e
Author: Apulupie <[email protected]>
AuthorDate: Fri Oct 9 17:58:06 2026 +0800

    fix(rmqctl): keep integer literals in studio payloads and name rejected MCP 
frames (#5262)
    
    Two defects in rmqctl's studio passthrough.
    
    - `request()` decoded the envelope's `data` section with `json.Unmarshal`, 
which folds every JSON
      number into a `float64`: 64-bit offsets and timestamps lose precision and 
large ids print in
      scientific notation, in the table/json/yaml output and in mutation 
summaries alike. It now decodes
      through a `json.Decoder` with `UseNumber()`, so numbers keep their 
literal form. (#5262)
    - `decodeMCPMessage` answered "missing method" for a JSON-RPC response 
frame (an `id` with no
      `method`), which reads as a malformed request. Response frames are now 
named as such together with
      the reason this transport rejects them; frames with neither `id` nor 
`method` keep the old error,
      and the request/notification paths are untouched. (#5273)
    
    `go test ./internal/studio/...` green, `make fmt` and `go vet` clean.
    
    Folded in #5273 (same author, same package).
---
 rmqctl/internal/studio/client.go           |  7 ++++-
 rmqctl/internal/studio/client_test.go      | 41 +++++++++++++++++++++++++++++-
 rmqctl/internal/studio/mcp_message.go      |  8 ++++++
 rmqctl/internal/studio/mcp_message_test.go | 30 ++++++++++++++++++++++
 4 files changed, 84 insertions(+), 2 deletions(-)

diff --git a/rmqctl/internal/studio/client.go b/rmqctl/internal/studio/client.go
index b459fc78d..cc5febfd8 100644
--- a/rmqctl/internal/studio/client.go
+++ b/rmqctl/internal/studio/client.go
@@ -158,7 +158,12 @@ func (c Client) request(ctx context.Context, target 
Target, method string, path
        if !hasJSONData(envelope.Data) {
                return nil
        }
-       if err := json.Unmarshal(envelope.Data, out); err != nil {
+       // Passthrough commands decode into an `any`, where encoding/json would 
represent every
+       // number as float64 and silently corrupt int64 values beyond 2^53 
(RocketMQ offsets and
+       // timestamps). UseNumber keeps the literal; decoding into typed struct 
fields is unaffected.
+       decoder := json.NewDecoder(bytes.NewReader(envelope.Data))
+       decoder.UseNumber()
+       if err := decoder.Decode(out); err != nil {
                return fmt.Errorf("invalid studio data: %w", err)
        }
        return nil
diff --git a/rmqctl/internal/studio/client_test.go 
b/rmqctl/internal/studio/client_test.go
index 7bc6fd464..4334d6d10 100644
--- a/rmqctl/internal/studio/client_test.go
+++ b/rmqctl/internal/studio/client_test.go
@@ -16,7 +16,14 @@
  */
 package studio
 
-import "testing"
+import (
+       "context"
+       "encoding/json"
+       "net/http"
+       "net/http/httptest"
+       "strings"
+       "testing"
+)
 
 // TestValidateServerSchemeAdaptiveHTTP verifies the adaptive scheme policy:
 // HTTPS is always accepted, plain HTTP is accepted for loopback / RFC1918
@@ -76,3 +83,35 @@ func TestIsPrivateHost(t *testing.T) {
                }
        }
 }
+
+// The passthrough commands decode the studio payload into an `any`, where 
encoding/json
+// would represent every number as float64 and silently corrupt int64 values 
beyond 2^53
+// (RocketMQ offsets and timestamps). The client must keep the literal via 
json.Number.
+func TestRequestPreservesLargeIntegersInPassthroughPayload(t *testing.T) {
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+               w.Header().Set("Content-Type", "application/json")
+               _, _ = 
w.Write([]byte(`{"code":200,"message":"ok","data":{"items":[{"msgOffset":9007199254740993}]}}`))
+       }))
+       defer server.Close()
+
+       client := NewClient(http.DefaultClient)
+       target := Target{
+               Server:     server.URL,
+               InstanceID: "instance-1",
+               Credential: Credential{AccessKey: "ak", SecretKey: "sk"},
+               Timeout:    DefaultTimeout,
+       }
+
+       var payload any
+       if err := client.request(context.Background(), target, http.MethodGet, 
"/api/brokers", nil, &payload); err != nil {
+               t.Fatalf("request returned error: %v", err)
+       }
+
+       encoded, err := json.Marshal(payload)
+       if err != nil {
+               t.Fatalf("marshal payload: %v", err)
+       }
+       if !strings.Contains(string(encoded), "9007199254740993") {
+               t.Fatalf("payload lost integer precision: %s", encoded)
+       }
+}
diff --git a/rmqctl/internal/studio/mcp_message.go 
b/rmqctl/internal/studio/mcp_message.go
index 49bbeccf0..9acefefc2 100644
--- a/rmqctl/internal/studio/mcp_message.go
+++ b/rmqctl/internal/studio/mcp_message.go
@@ -89,6 +89,14 @@ func decodeMCPMessage(payload json.RawMessage) 
(decodedMCPMessage, error) {
                }
                return decodedMCPMessage{notification: &notification}, nil
        }
+       if hasID {
+               // A frame with an id but no method is a JSON-RPC response, 
which the stdio
+               // proxy never originates. Name it explicitly instead of the 
misleading
+               // "missing method" (see the review on #4637).
+               return decodedMCPMessage{}, fmt.Errorf(
+                       "invalid MCP JSON-RPC message: response frame (id 
without method); " +
+                               "this transport only forwards client-originated 
requests and notifications")
+       }
        return decodedMCPMessage{}, fmt.Errorf("invalid MCP JSON-RPC message: 
missing method")
 }
 
diff --git a/rmqctl/internal/studio/mcp_message_test.go 
b/rmqctl/internal/studio/mcp_message_test.go
index 36341238b..aee3b0542 100644
--- a/rmqctl/internal/studio/mcp_message_test.go
+++ b/rmqctl/internal/studio/mcp_message_test.go
@@ -18,6 +18,7 @@ package studio
 
 import (
        "encoding/json"
+       "strings"
        "testing"
 )
 
@@ -123,3 +124,32 @@ func TestBindToolInstanceIgnoresNonToolCallMessages(t 
*testing.T) {
                t.Fatalf("initialize payload was modified: %s", bound)
        }
 }
+
+// A JSON-RPC response (id + result/error, no method) used to fall through to 
the
+// generic "missing method" error, which tells the caller nothing about what 
was
+// actually wrong. See the review on #4637.
+func TestDecodeMCPMessageRejectsResponseFrameWithExplicitError(t *testing.T) {
+       _, err := 
decodeMCPMessage([]byte(`{"jsonrpc":"2.0","id":7,"result":{"ok":true}}`))
+       if err == nil {
+               t.Fatal("expected an error for a response frame")
+       }
+       if !strings.Contains(err.Error(), "response") {
+               t.Fatalf("expected an explicit response-frame error, got: %v", 
err)
+       }
+       if strings.Contains(err.Error(), "missing method") {
+               t.Fatalf("the misleading missing-method error is still used: 
%v", err)
+       }
+
+       _, err = 
decodeMCPMessage([]byte(`{"jsonrpc":"2.0","id":8,"error":{"code":-32000,"message":"boom"}}`))
+       if err == nil || !strings.Contains(err.Error(), "response") {
+               t.Fatalf("expected an explicit error for an error frame too, 
got: %v", err)
+       }
+
+       // Requests and notifications must keep decoding.
+       if _, err := 
decodeMCPMessage([]byte(`{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{}}`));
 err != nil {
+               t.Fatalf("request frame should still decode: %v", err)
+       }
+       if _, err := 
decodeMCPMessage([]byte(`{"jsonrpc":"2.0","method":"notifications/initialized"}`));
 err != nil {
+               t.Fatalf("notification frame should still decode: %v", err)
+       }
+}

Reply via email to