recommend_for_project
Given a project description, identifies the appropriate MCP servers and explains why they are recommended.
Instructions
Given a project description, recommend the top MCP servers to install and explain why each one fits. Example: "Python FastAPI backend with PostgreSQL and JWT auth"
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| description | Yes |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |
Implementation Reference
- src/kothar/server.py:62-74 (handler)The recommend_for_project function is the actual tool handler. It is registered with FastMCP via @mcp.tool() decorator, takes a project description string, ensures the index is built, calls find_similar() for semantic search, and formats results using _format_results().
@mcp.tool() def recommend_for_project(description: str) -> str: """ Given a project description, recommend the top MCP servers to install and explain why each one fits. Example: "Python FastAPI backend with PostgreSQL and JWT auth" """ try: _ensure_index() results = find_similar(description, top_k=5) header = f"## Recommended MCP servers for: {description}\n\n" return header + _format_results(results, description) except Exception as e: return _error_response("generating recommendations", e) - src/kothar/server.py:63-63 (schema)The input schema is a single string parameter 'description' (no type annotations beyond str). The output schema is a str (formatted Markdown). FastMCP infers the schema automatically from the function signature.
def recommend_for_project(description: str) -> str: - src/kothar/server.py:62-62 (registration)The tool is registered with the FastMCP server using the @mcp.tool() decorator. The FastMCP instance is created on line 22: mcp = FastMCP("kothar")
@mcp.tool() - src/kothar/server.py:47-59 (helper)_format_results is a helper function that formats the list of server recommendations into Markdown. It calls generate_rationale() from the search module to produce explanations for each server.
def _format_results(results: list[dict], project_description: str) -> str: if not results: return "No matching MCP servers found. Try a more descriptive project description." lines = [] for i, r in enumerate(results, 1): rationale = generate_rationale(r, project_description) lines.append( f"{i}. **{r['name']}**\n" f" {r['url']}\n" f" {rationale}\n" ) return "\n".join(lines) - src/kothar/search.py:58-114 (helper)find_similar is the core search function called by recommend_for_project. It embeds the query, runs a cosine similarity search against the DuckDB index, and returns the top_k results with an adaptive fallback.
def find_similar( query: str, top_k: int = 10, exclude: list[str] | None = None, min_score: float = DEFAULT_MIN_SCORE, ) -> list[dict]: """ Return top_k servers most semantically similar to query. Each result: {name, url, description, category, score} """ model = _get_model() query_emb = _encode_query(model, query) excluded_lower = {n.lower() for n in (exclude or [])} con = get_connection() rows = con.execute( """ SELECT name, description, url, category, score FROM ( SELECT name, description, url, category, array_cosine_similarity(embedding, ?::FLOAT[384]) AS score FROM servers ) WHERE score >= ? ORDER BY score DESC """, [query_emb, min_score], ).fetchall() con.close() results = [] for name, desc, url, cat, score in rows: name_lower = name.lower() short_name = name_lower.split("/")[-1] if any( e == name_lower or e == short_name or e in name_lower for e in excluded_lower ): continue results.append( { "name": name, "url": url, "description": desc, "category": cat, "score": float(score), } ) if len(results) >= top_k: break # Adaptive fallback: if nothing cleared the threshold, return the closest # matches below it so callers never silently get zero results. if not results and min_score > 0: return find_similar(query, top_k=top_k, exclude=exclude, min_score=0.0) return results