Bug ID: CONTEXTFORGE-001 Component: ContextForge MCP Gateway Affected Version: v0.8.0, v1.0.0-BETA-1, v1.0.0-BETA-2 Severity: Medium Status: Confirmed in v1.0.0-BETA-2 (still valid) Reported: 2025-11-09 Last Validated: 2026-02-06
The POST /prompts/{prompt_id}/toggle endpoint does not reliably return the updated isActive state after toggling a prompt. When toggling a prompt from inactive to active (activate=true), the response returns the old state (isActive=false) instead of the new state (isActive=true).
POST /prompts/{prompt_id}/toggle?activate={true|false}
When calling the toggle endpoint with activate=true:
- The prompt's
is_activefield should be updated in the database totrue - The response should return the updated prompt with
isActive: true
When calling the toggle endpoint with activate=true:
- The prompt's
is_activefield IS updated in the database totrue✅ - The response returns the OLD state with
isActive: false❌
- Create a new prompt (starts as
isActive=true) - Toggle to inactive:
POST /prompts/{id}/toggle?activate=false- Response correctly shows
isActive=false✅
- Response correctly shows
- Toggle back to active:
POST /prompts/{id}/toggle?activate=true- Response INCORRECTLY shows
isActive=false❌
- Response INCORRECTLY shows
- List prompts with
include_inactive=true- Shows prompt with
isActive=true✅ (database state is correct)
- Shows prompt with
Conclusion: The database state is correct, but the API response returns stale/cached data.
Our SDK integration tests consistently fail on this scenario:
=== RUN TestPromptsService_Toggle/toggle_inactive_to_active
prompts_integration_test.go:220: Expected prompt to be active after toggle(true), got isActive=false
--- FAIL: TestPromptsService_Toggle/toggle_inactive_to_active (0.01s)
The test verifies the response from the toggle endpoint immediately after the call, confirming the bug.
File: mcpgateway/services/prompt_service.py
Method: toggle_prompt_status()
Lines: 847-899
async def toggle_prompt_status(self, db: Session, prompt_id: int, activate: bool) -> PromptRead:
try:
prompt = db.get(DbPrompt, prompt_id)
if not prompt:
raise PromptNotFoundError(f"Prompt not found: {prompt_id}")
if prompt.is_active != activate:
prompt.is_active = activate # Update state
prompt.updated_at = datetime.now(timezone.utc)
db.commit() # Commit to database
db.refresh(prompt) # Refresh object from DB
# ... notifications ...
prompt.team = self._get_team_name(db, prompt.team_id)
return PromptRead.model_validate(self._convert_db_prompt(prompt)) # ⚠️ PROBLEMThe _convert_db_prompt() method reads from the db_prompt object:
def _convert_db_prompt(self, db_prompt: DbPrompt) -> Dict[str, Any]:
# Line 240
return {
"is_active": db_prompt.is_active, # Reads from potentially stale object
# ...
}Despite calling db.refresh(prompt) on line 889, the object state may still be cached when _convert_db_prompt() is called on line 896. This is a known SQLAlchemy behavior where:
db.refresh()updates the object's state- BUT subsequent attribute accesses may hit the SQLAlchemy identity map cache
- The cache may not be properly expired after the team name lookup on line 895
The servers toggle endpoint works correctly because it manually constructs the response dict AFTER the refresh:
File: mcpgateway/services/server_service.py:863-929
async def toggle_server_status(self, db: Session, server_id: str, activate: bool) -> ServerRead:
# ... same pattern: get, check, update, commit, refresh ...
if server.is_active != activate:
server.is_active = activate
server.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(server)
# ...
# ✅ Manually builds dict AFTER refresh, ensuring fresh state
server_data = {
"id": server.id,
"name": server.name,
# ...
"is_active": server.is_active, # Guaranteed fresh from refresh
# ...
}
return ServerRead.model_validate(server_data)The key difference: servers builds the response dictionary inline AFTER the refresh, while prompts calls a helper method that may read cached state.
Add explicit session expiry before reading the prompt state:
# Line 895 - BEFORE _convert_db_prompt
db.expire(prompt) # Force SQLAlchemy to reload from DB
prompt.team = self._get_team_name(db, prompt.team_id)
return PromptRead.model_validate(self._convert_db_prompt(prompt))Add another db.refresh() right before the return:
# Line 895
prompt.team = self._get_team_name(db, prompt.team_id)
db.refresh(prompt) # Refresh again to ensure latest state
return PromptRead.model_validate(self._convert_db_prompt(prompt))Follow the servers pattern and build the dict manually:
# Replace _convert_db_prompt with inline dict construction
# (Similar to server_service.py:912-929)
prompt_data = {
"id": prompt.id,
"name": prompt.name,
"description": prompt.description,
"template": prompt.template,
"is_active": prompt.is_active, # Fresh from refresh
# ... other fields ...
}
return PromptRead.model_validate(prompt_data)This ensures the state is read immediately after refresh without any intervening operations.
There's also an inconsistency in toggle endpoint response formats across the API:
- Servers: Returns
ServerReaddirectly (unwrapped) - Tools, Resources, Prompts: Return wrapped format
{"status": "success", "message": "...", "<entity>": {...}}
While not the cause of this bug, this inconsistency should be addressed for API uniformity.
Severity: Medium
- ✅ Database state is correctly updated
- ✅ Subsequent API calls return correct state
- ❌ Toggle endpoint response shows stale state
- ❌ Clients relying on immediate response will see incorrect state
Affected Clients:
- Any SDK or client that relies on the toggle endpoint response to update UI state
- Automation scripts that chain operations based on toggle response
SDK users can work around this by:
-
Option 1: Ignore the toggle response and immediately fetch the prompt:
client.Prompts.Toggle(ctx, promptID, true) prompt, _, _ := client.Prompts.List(ctx, &contextforge.PromptListOptions{ IncludeInactive: true, }) // Find prompt in list to get fresh state
-
Option 2: Use the Update endpoint instead:
client.Prompts.Update(ctx, promptID, &contextforge.PromptUpdate{ // No need to set isActive - it's read-only in update })
-
Option 3: Trust that database state is correct and assume success if no error returned
Status: ✅ SDK implementation is correct
Our go-contextforge SDK correctly:
- Sends the toggle request with proper
activateparameter - Parses the wrapped response format
{"status": "success", "prompt": {...}} - Extracts the prompt data from the nested structure
The SDK integration test failure is expected given the ContextForge bug. All SDK unit tests pass.
- None known
- ContextForge Source:
mcpgateway/services/prompt_service.py:847-899 - Working Implementation:
mcpgateway/services/server_service.py:863-929 - SDK Integration Test:
test/integration/prompts_integration_test.go:196-223 - OpenAPI Spec: upstream
mcp-context-forgetag schema (no local snapshot maintained in this repo)
- Report this issue to the ContextForge team
- Request fix in next release
- Update SDK tests to document expected behavior once fixed
- Consider adding workaround documentation to SDK README
Validated: 2026-01-13
The code structure remains similar in v1.0.0-BETA-1, though with some changes:
- New location:
mcpgateway/services/prompt_service.py:1226-1314(was 847-899) - Field renamed:
is_active→enabled
The same pattern exists in v1.0.0-BETA-1:
# Line 1278-1279
db.commit()
db.refresh(prompt)
# Line 1313 - team lookup after refresh
prompt.team = self._get_team_name(db, prompt.team_id)
# Line 1314 - conversion that reads enabled field
return PromptRead.model_validate(self._convert_db_prompt(prompt))The _convert_db_prompt() method reads db_prompt.enabled at line 253.
Likely still present - The same SQLAlchemy session state pattern exists. Runtime verification needed to confirm, but the code structure suggests the bug persists.
Report Generated: 2025-11-09 Tested Against: ContextForge v0.8.0 Validated Against: ContextForge v1.0.0-BETA-2 Reporter: go-contextforge SDK Team
Validated: 2026-02-06
- Still Valid? Yes. The issue still reproduces in the
v1.0.0-BETA-2integration harness. - Is it actually a bug? Yes. Returning stale state from a mutation endpoint is a correctness bug.
- Runtime integration test
TestPromptsService_Toggle/toggle_inactive_to_activestill returnsisActive=falseimmediately aftertoggle?activate=true. - The persisted state is correct when read later, indicating response staleness rather than failed persistence.