Skip to content
Open
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
4 changes: 4 additions & 0 deletions internal/assets/commands/text/mcp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,10 @@ mcp.err-unknown-prompt:
short: 'unknown prompt: %s'
mcp.err-uri-required:
short: uri is required
mcp.err-input-too-long:
short: '%s exceeds maximum length (%d bytes)'
mcp.err-unknown-entry-type:
short: 'unknown entry type: %s'
mcp.format-watch-completed:
short: 'Completed: %s'
mcp.format-wrote:
Expand Down
20 changes: 14 additions & 6 deletions internal/config/embed/text/mcp_err.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,22 @@ const (
// DescKeyMCPErrTypeContentRequired is the text key for mcp err type content
// required messages.
DescKeyMCPErrTypeContentRequired = "mcp.err-type-content-required"
// DescKeyMCPErrQueryRequired is the text key for mcp err query required
// messages.
// DescKeyMCPErrQueryRequired is the text key for mcp err
// query required messages.
DescKeyMCPErrQueryRequired = "mcp.err-query-required"
// DescKeyMCPErrSearchRead is the text key for mcp err search read messages.
// DescKeyMCPErrSearchRead is the text key for mcp err
// search read messages.
DescKeyMCPErrSearchRead = "mcp.err-search-read"
// DescKeyMCPErrUnknownPrompt is the text key for mcp err unknown prompt
// messages.
// DescKeyMCPErrUnknownPrompt is the text key for mcp err
// unknown prompt messages.
DescKeyMCPErrUnknownPrompt = "mcp.err-unknown-prompt"
// DescKeyMCPErrURIRequired is the text key for mcp err uri required messages.
// DescKeyMCPErrURIRequired is the text key for mcp err
// uri required messages.
DescKeyMCPErrURIRequired = "mcp.err-uri-required"
// DescKeyMCPErrInputTooLong is the text key for mcp err
// input too long messages.
DescKeyMCPErrInputTooLong = "mcp.err-input-too-long"
// DescKeyMCPErrUnknownEntryType is the text key for mcp
// err unknown entry type messages.
DescKeyMCPErrUnknownEntryType = "mcp.err-unknown-entry-type"
)
15 changes: 15 additions & 0 deletions internal/config/mcp/cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,23 @@ const (

// DefaultSourceLimit is the max sessions returned by ctx_journal_source.
DefaultSourceLimit = 5
// MaxSourceLimit caps the source limit to prevent unbounded queries.
MaxSourceLimit = 100
// MinWordLen is the shortest word considered for overlap matching.
MinWordLen = 4
// MinWordOverlap is the minimum word matches to signal task completion.
MinWordOverlap = 2

// --- Input length limits (MCP-SAN.1) ---

// MaxContentLen is the maximum byte length for entry content fields.
MaxContentLen = 32_000
// MaxNameLen is the maximum byte length for tool/prompt/resource names.
MaxNameLen = 256
// MaxQueryLen is the maximum byte length for search queries.
MaxQueryLen = 1_000
// MaxCallerLen is the maximum byte length for caller identifiers.
MaxCallerLen = 128
// MaxURILen is the maximum byte length for resource URIs.
MaxURILen = 512
)
33 changes: 33 additions & 0 deletions internal/config/regex/sanitize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// / ctx: https://ctx.ist
// ,'`./ do you remember?
// `.,'\
// \ Copyright 2026-present Context contributors.
// SPDX-License-Identifier: Apache-2.0

package regex

import "regexp"

// SanEntryHeader matches entry headers like "## [2026-" in
// content sanitization (MCP-SAN.3).
var SanEntryHeader = regexp.MustCompile(
`(?m)^##\s+\[\d{4}-`,
)

// SanTaskCheckbox matches task checkboxes "- [ ]" and
// "- [x]" in content sanitization.
var SanTaskCheckbox = regexp.MustCompile(
`(?m)^-\s+\[[x ]\]`,
)

// SanConstitutionRule matches constitution rule format
// "- [ ] **Never" in content sanitization.
var SanConstitutionRule = regexp.MustCompile(
`(?m)^-\s+\[[x ]\]\s+\*\*[A-Z]`,
)

// SanSessionIDUnsafe matches characters not safe for session
// IDs in file paths: anything outside [a-zA-Z0-9._-].
var SanSessionIDUnsafe = regexp.MustCompile(
`[^a-zA-Z0-9._-]`,
)
13 changes: 13 additions & 0 deletions internal/config/sanitize/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// / ctx: https://ctx.ist
// ,'`./ do you remember?
// `.,'\
// \ Copyright 2026-present Context contributors.
// SPDX-License-Identifier: Apache-2.0

// Package sanitize defines string and length constants used by
// the sanitize layer.
//
// Constants are referenced by internal/sanitize via config/sanitize.*.
// Provides: [NullByte], [DotDot], [ForwardSlash], [Backslash],
// [HyphenReplace], [EscapePrefix], [MaxSessionIDLen].
package sanitize
34 changes: 34 additions & 0 deletions internal/config/sanitize/sanitize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// / ctx: https://ctx.ist
// ,'`./ do you remember?
// `.,'\
// \ Copyright 2026-present Context contributors.
// SPDX-License-Identifier: Apache-2.0

package sanitize

// Sanitize-layer string and length constants.
const (
// NullByte is the null character stripped from untrusted input.
NullByte = "\x00"

// DotDot is a path traversal sequence.
DotDot = ".."

// ForwardSlash is the forward slash stripped from session IDs.
ForwardSlash = "/"

// Backslash is the backslash stripped from session IDs.
Backslash = "\\"

// HyphenReplace is the replacement character for unsafe
// session ID characters.
HyphenReplace = "-"

// EscapePrefix is the backslash prefix for escaping Markdown
// structural patterns.
EscapePrefix = `\`

// MaxSessionIDLen is the maximum byte length for a session
// identifier.
MaxSessionIDLen = 128
)
98 changes: 98 additions & 0 deletions internal/entity/mcp_session_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// / ctx: https://ctx.ist
// ,'`./ do you remember?
// `.,'\
// \ Copyright 2026-present Context contributors.
// SPDX-License-Identifier: Apache-2.0

package entity

import (
"testing"
"time"
)

func TestNewMCPSession(t *testing.T) {
s := NewMCPSession()
if s.ToolCalls != 0 {
t.Errorf("ToolCalls = %d, want 0", s.ToolCalls)
}
if s.AddsPerformed == nil {
t.Fatal("AddsPerformed should be initialized")
}
if len(s.AddsPerformed) != 0 {
t.Errorf(
"AddsPerformed length = %d, want 0",
len(s.AddsPerformed),
)
}
if s.SessionStartedAt.IsZero() {
t.Error("SessionStartedAt should be set")
}
if len(s.PendingFlush) != 0 {
t.Errorf(
"PendingFlush length = %d, want 0",
len(s.PendingFlush),
)
}
}

func TestRecordToolCall(t *testing.T) {
s := NewMCPSession()
s.RecordToolCall()
if s.ToolCalls != 1 {
t.Errorf("ToolCalls = %d, want 1", s.ToolCalls)
}
s.RecordToolCall()
s.RecordToolCall()
if s.ToolCalls != 3 {
t.Errorf("ToolCalls = %d, want 3", s.ToolCalls)
}
}

func TestRecordAdd(t *testing.T) {
s := NewMCPSession()
s.RecordAdd("task")
s.RecordAdd("task")
s.RecordAdd("decision")
if s.AddsPerformed["task"] != 2 {
t.Errorf(
"task adds = %d, want 2",
s.AddsPerformed["task"],
)
}
if s.AddsPerformed["decision"] != 1 {
t.Errorf(
"decision adds = %d, want 1",
s.AddsPerformed["decision"],
)
}
}

func TestQueuePendingUpdate(t *testing.T) {
s := NewMCPSession()
now := time.Now()
s.QueuePendingUpdate(PendingUpdate{
Type: "task",
Content: "Build feature",
QueuedAt: now,
})
if len(s.PendingFlush) != 1 {
t.Fatalf(
"PendingFlush length = %d, want 1",
len(s.PendingFlush),
)
}
pu := s.PendingFlush[0]
if pu.Type != "task" {
t.Errorf(
"Type = %q, want %q",
pu.Type, "task",
)
}
if pu.Content != "Build feature" {
t.Errorf(
"Content = %q, want %q",
pu.Content, "Build feature",
)
}
}
16 changes: 16 additions & 0 deletions internal/err/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,19 @@ func UnknownEventType(eventType string) error {
eventType,
)
}

// InputTooLong returns an error when input exceeds the allowed
// length.
//
// Parameters:
// - field: the field name that is too long
// - maxLen: the maximum allowed length
//
// Returns:
// - error: "<field> exceeds maximum length of <maxLen>"
func InputTooLong(field string, maxLen int) error {
return fmt.Errorf(
desc.Text(text.DescKeyMCPErrInputTooLong),
field, maxLen,
)
}
Loading
Loading