MCP 2026-07-28 changes the mental model for remote transport. The current profile is not a long-lived protocol session with a permanent SSE channel. It is a sequence of independent JSON-RPC requests sent to one HTTP endpoint, each carrying enough metadata to be understood on its own.

SSE still exists, but in a narrower role:

A POST can receive either a single JSON response or a request-scoped SSE response. GET is not a general event channel, and broken response streams are not resumed.

This distinction determines how a Go server handles state, authentication, cancellation, retries, proxies, and horizontal scaling.

Key Takeaways

  • Use one POST endpoint for new 2026-07-28 remote servers.
  • Run the official Go SDK's Streamable HTTP handler in stateless mode.
  • Let the SDK validate protocol metadata; do not hand-roll JSON-RPC framing.
  • Treat SSE as a response encoding for the current request.
  • Use subscriptions/listen for long-lived server update delivery.
  • Reissue interrupted requests with new JSON-RPC IDs; do not use Last-Event-ID.
  • Make write retries safe with idempotency and status lookup.
  • Authenticate and authorize every request, independent of the connection.
  • Store business state in durable systems and exchange opaque handles.
  • Test through the real proxy and load balancer, not only on localhost.

Transport Profiles Must Not Be Mixed

Three HTTP patterns appear in MCP material:

Profile Shape Status for new work
Legacy SSE (2024-11-05) GET /sse plus session-specific POST /message Compatibility only
Earlier Streamable HTTP POST plus optional standalone GET, protocol sessions, resumable SSE Older negotiated revisions
2026-07-28 Streamable HTTP One POST endpoint, no protocol session, JSON or request SSE Current profile

A generic route named /sse does not reveal which protocol it implements. Compatibility must be explicit in configuration, supported-version lists, endpoint ownership, telemetry, and tests.

Do not accept a 2026-07-28 request and then silently apply legacy session behavior. Likewise, do not expose a legacy route without its session-binding, Origin, authentication, and lifecycle controls.

Request Lifecycle

The current request flow is:

text
client
  POST /mcp
  Content-Type: application/json
  Accept: application/json, text/event-stream
  JSON-RPC request with required _meta
        |
        v
server validates transport + protocol + auth
        |
        +-> application/json response
        |
        `-> text/event-stream for this POST only

For 2026-07-28:

  • server/discover replaces the old initialization handshake;
  • _meta carries protocol version, client information, and capabilities on requests;
  • HTTP headers mirror selected body fields and mismatches must be rejected;
  • the server response includes resultType;
  • the request ends when its response ends.

Use the SDK to produce exact metadata and wire errors. Protocol fields are interoperability contracts, not application extension points.

JSON Response or Request-Scoped SSE

Choose based on behavior:

Response Use when Trade-off
application/json One bounded result with no intermediate server messages Simpler proxying and observability
text/event-stream The request emits multiple protocol messages before completion Requires streaming-safe infrastructure

SSE is not automatically a progress API. Progress, notifications, and server-to-client requests still need protocol support and client capabilities. Avoid sending comments or custom event types that a conforming MCP client does not understand.

For SSE responses:

  • flush complete events, never partial JSON;
  • stop work when the request context is cancelled;
  • bound event count, total bytes, and wall time;
  • avoid unbounded per-request queues;
  • preserve JSON-RPC correlation;
  • keep HTTP write timeouts compatible with intended stream duration.

Discovery and Per-Request Metadata

server/discover lets a client learn supported protocol versions before sending ordinary requests. In 2026-07-28, each request remains self-describing through reserved _meta keys.

This is not permission negotiation. Client capabilities state what the client can process; they do not prove identity, user consent, or authority. A server should:

  1. reject unsupported protocol versions;
  2. validate required metadata and header/body consistency;
  3. authenticate the HTTP request;
  4. authorize the named MCP operation and business resource;
  5. execute under bounded context;
  6. return a protocol-valid result.

Pin the protocol versions accepted by the server. Broad compatibility increases the state space and should be a conscious product decision.

Business State Without Protocol Sessions

Stateless transport means any instance can handle the next request. Durable workflows still need state:

text
requestState or business handle
  -> principal
  -> tenant
  -> operation
  -> canonical arguments
  -> resource version
  -> expiry

Store this mapping server-side or use an authenticated encrypted token. Validate scope and expiry on every use.

Never use:

  • source IP or TCP connection as identity;
  • a client-chosen session ID as authorization;
  • process memory as the only workflow store;
  • a model-generated “approved: true” field;
  • a raw database primary key with no object-level authorization.

Explicit Subscriptions

The old standalone GET stream has been replaced by explicit subscription methods for long-lived updates. A client subscribes to a supported stream and invokes subscriptions/listen to receive events.

That gives the lifecycle a named protocol contract:

  • subscription creation is authorized;
  • a subscription has an explicit identity and scope;
  • listening can be cancelled and retried;
  • server changes do not depend on an unrelated connection;
  • limits can be applied per principal and subscription.

Subscription data still needs retention, overflow, redelivery, and ordering policies. “Uses SSE” does not define any of those semantics.

Retries, Idempotency, and Cancellation

The 2026-07-28 transport removes SSE event IDs and Last-Event-ID resumption. If a request-scoped stream breaks, the client creates a new JSON-RPC request.

Classify before retrying:

Operation Strategy
Pure read Retry with exponential backoff inside a deadline
Expensive read Retry only if budget remains; consider a cached artifact
Idempotent write Reuse the business idempotency key, not the JSON-RPC ID
Ambiguous write Query operation status before retrying
Non-idempotent write Require explicit recovery or human review

The JSON-RPC request ID correlates one protocol call; it is not a durable business idempotency key.

Enable request-cancellation propagation only when handlers honor context.Context. Cancellation does not roll back a committed transaction. For side effects, define a transaction boundary and record the result under the idempotency key before acknowledging success.

Runnable Go Server

The official Go SDK v1.8.0 supports MCP 2026-07-28 and requires Go 1.25 or newer. This example pins the server to that protocol, exposes one POST endpoint, enables stateless mode and request cancellation, caps request bodies, and shuts down cleanly.

It binds only to loopback. Internet exposure requires OAuth resource-server middleware, TLS, explicit audience and scope validation, and deployment-specific origin/proxy controls.

go
package main

import (
	"context"
	"log"
	"net/http"
	"os/signal"
	"syscall"
	"time"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

type HealthInput struct{}

type HealthOutput struct {
	Status string `json:"status"`
}

func health(
	ctx context.Context,
	request *mcp.CallToolRequest,
	input HealthInput,
) (*mcp.CallToolResult, HealthOutput, error) {
	if err := ctx.Err(); err != nil {
		return nil, HealthOutput{}, err
	}
	return nil, HealthOutput{Status: "ok"}, nil
}

func newMCPServer() *mcp.Server {
	server := mcp.NewServer(
		&mcp.Implementation{
			Name:    "health-server",
			Version: "v1.0.0",
		},
		&mcp.ServerOptions{
			SupportedProtocolVersions: []string{"2026-07-28"},
		},
	)
	mcp.AddTool(server, &mcp.Tool{
		Name:        "service_health_get",
		Description: "Return this service's current health status. " +
			"Use only for availability checks; it does not inspect dependencies.",
		Annotations: &mcp.ToolAnnotations{
			ReadOnlyHint: true,
		},
	}, health)
	return server
}

func main() {
	mcpHandler := mcp.NewStreamableHTTPHandler(
		func(request *http.Request) *mcp.Server {
			return newMCPServer()
		},
		&mcp.StreamableHTTPOptions{
			Stateless:                    true,
			MaxRequestBodyBytes:          1 << 20,
			PropagateRequestCancellation: true,
		},
	)

	mux := http.NewServeMux()
	mux.Handle("/mcp", mcpHandler)
	crossOriginProtection := http.NewCrossOriginProtection()

	httpServer := &http.Server{
		Addr:              "127.0.0.1:8080",
		Handler:           crossOriginProtection.Handler(mux),
		ReadHeaderTimeout: 5 * time.Second,
		IdleTimeout:       90 * time.Second,
	}

	ctx, stop := signal.NotifyContext(
		context.Background(),
		syscall.SIGINT,
		syscall.SIGTERM,
	)
	defer stop()

	serverErrors := make(chan error, 1)
	go func() {
		serverErrors <- httpServer.ListenAndServe()
	}()

	select {
	case <-ctx.Done():
	case err := <-serverErrors:
		if err != nil && err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}

	shutdownContext, cancel := context.WithTimeout(
		context.Background(),
		10*time.Second,
	)
	defer cancel()
	if err := httpServer.Shutdown(shutdownContext); err != nil {
		log.Fatal(err)
	}
}

Creating a server per request matches the stateless model. The SDK can share its schema cache through server options when a large catalog makes reflection cost material; measure before optimizing. A production getServer function can also inject a request-scoped authenticated principal into service dependencies, but it must not build policy from model arguments.

Install and run:

text
go mod init example.com/mcp-health
go get github.com/modelcontextprotocol/[email protected]
go run .

Use an MCP client or Inspector instead of a handcrafted curl call so required _meta, mirrored headers, discovery, and JSON-RPC framing stay consistent.

Authentication and Authorization

For remote servers, follow the MCP authorization specification and OAuth resource-server requirements. The Go SDK provides bearer-token middleware primitives, but deployment still needs correct issuer, audience, expiry, and scope policy.

Keep the layers distinct:

text
TLS -> HTTP authentication -> MCP protocol validation
    -> tool authorization -> object authorization -> business policy

Validate every POST. A previous request, open connection, or subscription does not authorize the next operation. Avoid forwarding the user's bearer token to downstream services; exchange it for audience-scoped credentials where possible.

Return 401 with the required challenge for missing or invalid authentication and 403 for an authenticated principal that lacks permission, while avoiding resource-existence leaks.

Proxy and Server Configuration

Validate the complete path:

  • proxy request buffering is compatible with streaming responses;
  • intermediary idle timeouts exceed supported request duration;
  • compression does not delay small SSE events;
  • HTTP/2 and HTTP/3 behavior is tested where enabled;
  • request body limits apply to chunked and HTTP/2 uploads;
  • trusted proxy headers are accepted only from known hops;
  • DNS rebinding and Origin policy protect local servers;
  • shutdown drains or cancels in-flight requests within a bound.

Do not set a short global WriteTimeout on an endpoint that intentionally streams longer than that value. Prefer request deadlines and per-operation budgets.

Legacy Compatibility

If old clients still require GET /sse plus POST /message:

  1. expose a separate compatibility endpoint or service;
  2. pin the old protocol version;
  3. bind server-issued session IDs to the authenticated principal;
  4. validate Origin and protect local endpoints from DNS rebinding;
  5. bound queues, heartbeats, idle sessions, and reconnect rates;
  6. publish a removal metric and migration deadline.

Do not copy a partial http.Flusher sample and call it an MCP server. The transport also needs JSON-RPC validation, correlation, cancellation, authorization, backpressure, and lifecycle behavior.

Test Matrix

Protocol

  • server/discover, supported and unsupported versions;
  • required _meta and header/body mismatch;
  • JSON and request-scoped SSE responses;
  • tool call success, tool error, and input_required;
  • cancellation and response completion;
  • subscriptions/listen lifecycle.

HTTP and Infrastructure

  • wrong method, media type, and oversized body;
  • slow upload and slow consumer;
  • proxy buffering and idle timeout;
  • disconnect before and after a side effect commits;
  • graceful shutdown with active requests;
  • concurrent requests routed to different instances.

Security

  • missing, expired, wrong-audience, and insufficient-scope tokens;
  • cross-tenant identifiers;
  • forged state handles;
  • DNS rebinding and untrusted forwarded headers;
  • result-size abuse and prompt injection in downstream content.

Reliability

  • duplicate idempotency keys;
  • retry after lost JSON and SSE responses;
  • downstream timeout and rate limit;
  • subscription overflow;
  • process restart between workflow steps.

Production Checklist

  • [ ] Protocol and SDK versions are pinned.
  • [ ] 2026-07-28 requests use stateless Streamable HTTP.
  • [ ] Only POST is exposed on the current endpoint.
  • [ ] Required metadata and mirrored headers are SDK-validated.
  • [ ] Request body, response bytes, events, duration, and concurrency are bounded.
  • [ ] Authentication and object authorization run on every request.
  • [ ] Handlers propagate cancellation.
  • [ ] Write operations use durable idempotency and status lookup.
  • [ ] Business state uses scoped, expiring handles.
  • [ ] Subscriptions have authorization, retention, and overflow policy.
  • [ ] Proxy streaming behavior is verified under load.
  • [ ] Legacy compatibility is isolated and measured.

Conclusion

Modern MCP transport is simpler when its boundaries are respected: one POST endpoint, self-describing requests, stateless routing, and JSON or request-scoped SSE responses. The simplicity disappears if legacy sessions, hidden connection state, and unsafe retries are carried forward.

Use the official SDK for protocol framing, then spend engineering effort on the controls the protocol cannot supply: identity, authorization, idempotency, durable state, limits, observability, and failure recovery.

For the Tool contract behind the endpoint, see MCP Tool design. For host-side execution policy, read the LLM tool-calling guide.

Primary Sources