diff --git a/src/mcp.zig b/src/mcp.zig
index eef0de2f..ad3051b0 100644
--- a/src/mcp.zig
+++ b/src/mcp.zig
@@ -2197,6 +2197,59 @@ fn extractContextCandidates(task: []const u8, alloc: std.mem.Allocator, out: *st
     }
 }
 
+// #570: fallback for tasks with no identifier-shaped token. Plain words
+// (≥4 chars, glue/generic words dropped) sorted longest-first — longer words
+// are more specific ("ranking" beats "fix") — capped like the identifier pass.
+fn extractContextFallbackWords(task: []const u8, alloc: std.mem.Allocator, out: *std.ArrayList([]const u8)) void {
+    const stop = [_][]const u8{
+        "that",  "this",  "with",    "from",      "into",   "when",   "where",
+        "what",  "which", "then",    "them",      "they",   "have",   "will",
+        "should", "would", "could",  "make",      "makes",  "using",  "used",
+        "does",  "like",  "also",    "than",      "each",   "more",   "most",
+        "some",  "such",  "very",    "just",      "been",   "being",  "about",
+        "after", "before", "while",  "there",     "their",  "other",  "only",
+        "over",  "under", "between", "improve",   "implement", "ensure", "change",
+        "update",
+    };
+    var words: std.ArrayList([]const u8) = .empty;
+    defer words.deinit(alloc);
+    var seen = std.StringHashMap(void).init(alloc);
+    defer seen.deinit();
+    var i: usize = 0;
+    while (i < task.len) {
+        if (isContextIdentStart(task[i])) {
+            const start = i;
+            while (i < task.len and isContextIdentCont(task[i])) : (i += 1) {}
+            const tok = task[start..i];
+            if (tok.len >= 4 and tok.len <= 64 and !seen.contains(tok)) {
+                var is_stop = false;
+                for (stop) |s| {
+                    if (std.ascii.eqlIgnoreCase(tok, s)) {
+                        is_stop = true;
+                        break;
+                    }
+                }
+                if (!is_stop) {
+                    seen.put(tok, {}) catch {};
+                    words.append(alloc, tok) catch {};
+                }
+            }
+            continue;
+        }
+        i += 1;
+    }
+    std.sort.block([]const u8, words.items, {}, struct {
+        pub fn lessThan(_: void, a: []const u8, b: []const u8) bool {
+            if (a.len != b.len) return a.len > b.len;
+            return std.mem.lessThan(u8, a, b);
+        }
+    }.lessThan);
+    for (words.items) |w| {
+        out.append(alloc, w) catch {};
+        if (out.items.len >= CONTEXT_MAX_CANDIDATES) return;
+    }
+}
+
 fn handleContext(io: std.Io, alloc: std.mem.Allocator, args: *const std.json.ObjectMap, out: *std.ArrayList(u8), explorer: *Explorer, project_root: []const u8) void {
     const task = getStr(args, "task") orelse {
         out.appendSlice(alloc, "error: missing 'task' argument") catch {};
@@ -2253,6 +2306,13 @@ fn handleContext(io: std.Io, alloc: std.mem.Allocator, args: *const std.json.Obj
 
     var candidates: std.ArrayList([]const u8) = .empty;
     extractContextCandidates(task, A, &candidates);
+    if (candidates.items.len == 0) {
+        // #570: all-lowercase tasks ("fix search ranking") carry no
+        // identifier-shaped token. Fall back to the task's plain words so the
+        // composer orients instead of dead-ending — natural language is the
+        // documented input shape.
+        extractContextFallbackWords(task, A, &candidates);
+    }
     if (candidates.items.len == 0) {
         out.appendSlice(alloc, "no candidate identifiers found in task — include symbol names (camelCase or snake_case) or \"quoted strings\" so the composer can extract keywords") catch {};
         return;
diff --git a/src/test_mcp.zig b/src/test_mcp.zig
index 47f409ba..033254df 100644
--- a/src/test_mcp.zig
+++ b/src/test_mcp.zig
@@ -1982,3 +1982,39 @@ test "issue-538: temp roots are indexable only when CODEDB_ALLOW_TEMP opts in" {
     try testing.expect(!root_policy.isIndexableRoot("/usr/local/bin"));
     try testing.expect(!root_policy.isIndexableRoot("/"));
 }
+
+
+test "issue-570: codedb_context falls back to plain words for all-lowercase tasks" {
+    // 'fix search ranking' has no identifier-shaped token (no snake_case, no
+    // camelCase, no quotes), so extractContextCandidates finds nothing and the
+    // handler dead-ends with 'no candidate identifiers found'. Natural-language
+    // tasks are the documented input shape — the composer must fall back to
+    // plain words instead of erroring.
+    var explorer = Explorer.init(testing.allocator, Explorer.DEFAULT_CONTENT_CACHE_CAPACITY);
+    defer explorer.deinit();
+    try explorer.indexFile("src/ranking.zig", "pub fn rankingBoost() void {}\n");
+
+    var store = Store.init(testing.allocator);
+    defer store.deinit();
+    var agents = AgentRegistry.init(testing.allocator);
+    defer agents.deinit();
+    _ = try agents.register("__filesystem__");
+
+    var bench_ctx = mcp_mod.BenchContext.init(testing.allocator, ".", Explorer.DEFAULT_CONTENT_CACHE_CAPACITY);
+    defer bench_ctx.deinit();
+
+    const args_json =
+        \\{"task":"fix search ranking"}
+    ;
+    const parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, args_json, .{});
+    defer parsed.deinit();
+
+    var out: std.ArrayList(u8) = .empty;
+    defer out.deinit(testing.allocator);
+    bench_ctx.runDispatch(io, testing.allocator, .codedb_context, &parsed.value.object, &out, &store, &explorer, &agents);
+
+    // An all-lowercase task must not dead-end…
+    try testing.expect(std.mem.indexOf(u8, out.items, "no candidate identifiers") == null);
+    // …its longest meaningful word ('ranking') must drive the composer.
+    try testing.expect(std.mem.indexOf(u8, out.items, "ranking") != null);
+}
