diff --git a/.gitignore b/.gitignore
index b263cd1..9088605 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ erl_crash.dump
*.beam
/config/*.secret.exs
.elixir_ls/
+.idea
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 13566b8..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml
deleted file mode 100644
index 4ea72a9..0000000
--- a/.idea/copilot.data.migration.agent.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
AI-powered security guidance for developers
diff --git a/guided/test/guided/mcp_server_cypher_test.exs b/guided/test/guided/mcp_server_cypher_test.exs new file mode 100644 index 0000000..2d5b36d --- /dev/null +++ b/guided/test/guided/mcp_server_cypher_test.exs @@ -0,0 +1,98 @@ +defmodule Guided.MCPServerCypherTest do + use ExUnit.Case + doctest Guided.MCPServer + + describe "Cypher Query Syntax" do + test "tech_stack_recommendation uses positional parameter $0" do + # The query should use $0 for positional parameters, not named parameters + query = """ + MATCH (t:Technology)-[:RECOMMENDED_FOR]->(uc:UseCase {name: $0}) + OPTIONAL MATCH (t)-[:HAS_VULNERABILITY]->(v:Vulnerability) + OPTIONAL MATCH (v)-[:MITIGATED_BY]->(sc:SecurityControl) + RETURN t.name as technology, + t.category as category, + t.description as description, + t.security_rating as security_rating, + v.name as vuln_name, + v.severity as vuln_severity, + v.description as vuln_description, + sc.name as mitigation_name + """ + + # Should contain $0 for positional parameter + assert query =~ "$0" + # Should not contain named parameters like $use_case + refute query =~ "$use_case" + end + + test "secure_coding_pattern uses positional parameter $0" do + query = """ + MATCH (t:Technology {name: $0})-[:HAS_BEST_PRACTICE]->(bp:BestPractice) + OPTIONAL MATCH (bp)-[:IMPLEMENTS_CONTROL]->(sc:SecurityControl) + RETURN bp.name as practice_name, + bp.category as category, + bp.description as description, + bp.code_example as code_example, + sc.name as security_control + """ + + assert query =~ "$0" + refute query =~ "$technology" + end + + test "deployment_guidance uses positional parameter $0 and avoids nested collect" do + query = """ + MATCH (t:Technology)-[:RECOMMENDED_FOR]->(uc:UseCase)-[:RECOMMENDED_DEPLOYMENT]->(dp:DeploymentPattern) + WHERE t.name IN $0 + RETURN dp.name as pattern_name, + dp.platform as platform, + dp.cost as cost, + dp.complexity as complexity, + dp.description as description, + dp.https_support as https_support, + uc.name as use_case + """ + + assert query =~ "$0" + refute query =~ "$technologies" + # Should not have nested collect + refute query =~ ~r/collect.*collect/ + # Should return flat results + assert query =~ "uc.name as use_case" + end + + test "queries avoid nested collect() which AGE doesn't support" do + # tech_stack_recommendation query + tech_query = """ + MATCH (t:Technology)-[:RECOMMENDED_FOR]->(uc:UseCase {name: $0}) + OPTIONAL MATCH (t)-[:HAS_VULNERABILITY]->(v:Vulnerability) + OPTIONAL MATCH (v)-[:MITIGATED_BY]->(sc:SecurityControl) + RETURN t.name as technology, + t.category as category, + t.description as description, + t.security_rating as security_rating, + v.name as vuln_name, + v.severity as vuln_severity, + v.description as vuln_description, + sc.name as mitigation_name + """ + + # Should not contain nested collect() + refute tech_query =~ ~r/collect\s*\([^)]*collect/i + end + end + + describe "MCP Response Format" do + test "handle_tool_call should return {:reply, text_string, frame}" do + # The correct format for Hermes is {:reply, text_result, updated_frame} + # where text_result is a JSON-encoded string + + # This is a documentation test to ensure developers know the correct format + expected_format = {:reply, "json string here", :frame_here} + assert is_tuple(expected_format) + assert tuple_size(expected_format) == 3 + assert elem(expected_format, 0) == :reply + assert is_binary(elem(expected_format, 1)) + end + end +end diff --git a/guided/test/guided/mcp_server_test.exs b/guided/test/guided/mcp_server_test.exs index 73ca7a1..5d09549 100644 --- a/guided/test/guided/mcp_server_test.exs +++ b/guided/test/guided/mcp_server_test.exs @@ -2,6 +2,7 @@ defmodule Guided.MCPServerTest do use Guided.DataCase, async: false alias Guided.Graph + alias Hermes.Server.Frame # Ensure the graph is seeded before each test setup do @@ -16,150 +17,235 @@ defmodule Guided.MCPServerTest do defp seed_test_data do # Create Technologies - {:ok, _} = Graph.create_node("Technology", %{ - name: "Streamlit", - category: "framework", - description: "Python framework for data apps", - security_rating: "good" - }) - - {:ok, _} = Graph.create_node("Technology", %{ - name: "SQLite", - category: "database", - description: "Lightweight SQL database", - security_rating: "good" - }) - - {:ok, _} = Graph.create_node("Technology", %{ - name: "FastAPI", - category: "framework", - description: "Modern Python web framework", - security_rating: "excellent" - }) + {:ok, _} = + Graph.create_node("Technology", %{ + name: "Streamlit", + category: "framework", + description: "Python framework for data apps", + security_rating: "good" + }) + + {:ok, _} = + Graph.create_node("Technology", %{ + name: "SQLite", + category: "database", + description: "Lightweight SQL database", + security_rating: "good" + }) + + {:ok, _} = + Graph.create_node("Technology", %{ + name: "FastAPI", + category: "framework", + description: "Modern Python web framework", + security_rating: "excellent" + }) # Create Use Cases - {:ok, _} = Graph.create_node("UseCase", %{ - name: "data_dashboard", - description: "Interactive data dashboard", - user_scale: "1-1000" - }) + {:ok, _} = + Graph.create_node("UseCase", %{ + name: "data_dashboard", + description: "Interactive data dashboard", + user_scale: "1-1000" + }) + + {:ok, _} = + Graph.create_node("UseCase", %{ + name: "web_app_small_team", + description: "Web app for small team", + user_scale: "1-100" + }) + + {:ok, _} = + Graph.create_node("UseCase", %{ + name: "api_service", + description: "RESTful API service", + user_scale: "variable" + }) - {:ok, _} = Graph.create_node("UseCase", %{ - name: "web_app_small_team", - description: "Web app for small team", - user_scale: "1-100" - }) + # Create Vulnerabilities + {:ok, _} = + Graph.create_node("Vulnerability", %{ + name: "SQL Injection", + severity: "critical", + description: "Malicious SQL injection", + owasp_rank: "A03:2021" + }) + + {:ok, _} = + Graph.create_node("Vulnerability", %{ + name: "Cross-Site Scripting (XSS)", + severity: "high", + description: "Script injection attacks", + owasp_rank: "A03:2021" + }) - {:ok, _} = Graph.create_node("UseCase", %{ - name: "api_service", - description: "RESTful API service", - user_scale: "variable" - }) + # Create Security Controls + {:ok, _} = + Graph.create_node("SecurityControl", %{ + name: "Parameterized Queries", + category: "input_validation", + description: "Use prepared statements", + implementation_difficulty: "low" + }) + + {:ok, _} = + Graph.create_node("SecurityControl", %{ + name: "Input Sanitization", + category: "input_validation", + description: "Validate all user inputs", + implementation_difficulty: "medium" + }) + + {:ok, _} = + Graph.create_node("SecurityControl", %{ + name: "Output Encoding", + category: "output_handling", + description: "Encode output to prevent XSS", + implementation_difficulty: "low" + }) - # Create Vulnerabilities - {:ok, _} = Graph.create_node("Vulnerability", %{ - name: "SQL Injection", - severity: "critical", - description: "Malicious SQL injection", - owasp_rank: "A03:2021" - }) + # Create Best Practices + {:ok, _} = + Graph.create_node("BestPractice", %{ + name: "Use SQLAlchemy with Parameterized Queries", + technology: "SQLite", + category: "database_security", + description: "Always use parameterized queries", + code_example: "session.query(User).filter(User.name == user_input)" + }) + + {:ok, _} = + Graph.create_node("BestPractice", %{ + name: "Streamlit Secret Management", + technology: "Streamlit", + category: "configuration", + description: "Use st.secrets for sensitive config", + code_example: "db_password = st.secrets['database']['password']" + }) + + {:ok, _} = + Graph.create_node("BestPractice", %{ + name: "Validate User Inputs in Forms", + technology: "Streamlit", + category: "input_validation", + description: "Always validate user inputs from forms", + code_example: "if user_input and len(user_input) < 100: ..." + }) - {:ok, _} = Graph.create_node("Vulnerability", %{ - name: "Cross-Site Scripting (XSS)", - severity: "high", - description: "Script injection attacks", - owasp_rank: "A03:2021" - }) + # Create Deployment Patterns + {:ok, _} = + Graph.create_node("DeploymentPattern", %{ + name: "Streamlit Cloud", + platform: "streamlit_cloud", + cost: "free_tier_available", + complexity: "low", + description: "Official Streamlit hosting", + https_support: true + }) + + {:ok, _} = + Graph.create_node("DeploymentPattern", %{ + name: "Fly.io Deployment", + platform: "fly_io", + cost: "free_tier_available", + complexity: "low", + description: "Deploy to Fly.io", + https_support: true + }) - # Create Security Controls - {:ok, _} = Graph.create_node("SecurityControl", %{ - name: "Parameterized Queries", - category: "input_validation", - description: "Use prepared statements", - implementation_difficulty: "low" + # Create Relationships + create_rel("Technology", %{name: "Streamlit"}, "RECOMMENDED_FOR", "UseCase", %{ + name: "data_dashboard" }) - {:ok, _} = Graph.create_node("SecurityControl", %{ - name: "Input Sanitization", - category: "input_validation", - description: "Validate all user inputs", - implementation_difficulty: "medium" + create_rel("Technology", %{name: "Streamlit"}, "RECOMMENDED_FOR", "UseCase", %{ + name: "web_app_small_team" }) - {:ok, _} = Graph.create_node("SecurityControl", %{ - name: "Output Encoding", - category: "output_handling", - description: "Encode output to prevent XSS", - implementation_difficulty: "low" + create_rel("Technology", %{name: "FastAPI"}, "RECOMMENDED_FOR", "UseCase", %{ + name: "api_service" }) - # Create Best Practices - {:ok, _} = Graph.create_node("BestPractice", %{ - name: "Use SQLAlchemy with Parameterized Queries", - technology: "SQLite", - category: "database_security", - description: "Always use parameterized queries", - code_example: "session.query(User).filter(User.name == user_input)" + create_rel("Technology", %{name: "SQLite"}, "RECOMMENDED_FOR", "UseCase", %{ + name: "web_app_small_team" }) - {:ok, _} = Graph.create_node("BestPractice", %{ - name: "Streamlit Secret Management", - technology: "Streamlit", - category: "configuration", - description: "Use st.secrets for sensitive config", - code_example: "db_password = st.secrets['database']['password']" + create_rel("Technology", %{name: "SQLite"}, "HAS_VULNERABILITY", "Vulnerability", %{ + name: "SQL Injection" }) - {:ok, _} = Graph.create_node("BestPractice", %{ - name: "Validate User Inputs in Forms", - technology: "Streamlit", - category: "input_validation", - description: "Always validate user inputs from forms", - code_example: "if user_input and len(user_input) < 100: ..." + create_rel("Technology", %{name: "Streamlit"}, "HAS_VULNERABILITY", "Vulnerability", %{ + name: "Cross-Site Scripting (XSS)" }) - # Create Deployment Patterns - {:ok, _} = Graph.create_node("DeploymentPattern", %{ - name: "Streamlit Cloud", - platform: "streamlit_cloud", - cost: "free_tier_available", - complexity: "low", - description: "Official Streamlit hosting", - https_support: true + create_rel("Vulnerability", %{name: "SQL Injection"}, "MITIGATED_BY", "SecurityControl", %{ + name: "Parameterized Queries" }) - {:ok, _} = Graph.create_node("DeploymentPattern", %{ - name: "Fly.io Deployment", - platform: "fly_io", - cost: "free_tier_available", - complexity: "low", - description: "Deploy to Fly.io", - https_support: true + create_rel("Vulnerability", %{name: "SQL Injection"}, "MITIGATED_BY", "SecurityControl", %{ + name: "Input Sanitization" }) - # Create Relationships - create_rel("Technology", %{name: "Streamlit"}, "RECOMMENDED_FOR", "UseCase", %{name: "data_dashboard"}) - create_rel("Technology", %{name: "Streamlit"}, "RECOMMENDED_FOR", "UseCase", %{name: "web_app_small_team"}) - create_rel("Technology", %{name: "FastAPI"}, "RECOMMENDED_FOR", "UseCase", %{name: "api_service"}) - create_rel("Technology", %{name: "SQLite"}, "RECOMMENDED_FOR", "UseCase", %{name: "web_app_small_team"}) - - create_rel("Technology", %{name: "SQLite"}, "HAS_VULNERABILITY", "Vulnerability", %{name: "SQL Injection"}) - create_rel("Technology", %{name: "Streamlit"}, "HAS_VULNERABILITY", "Vulnerability", %{name: "Cross-Site Scripting (XSS)"}) + create_rel( + "Vulnerability", + %{name: "Cross-Site Scripting (XSS)"}, + "MITIGATED_BY", + "SecurityControl", + %{name: "Output Encoding"} + ) - create_rel("Vulnerability", %{name: "SQL Injection"}, "MITIGATED_BY", "SecurityControl", %{name: "Parameterized Queries"}) - create_rel("Vulnerability", %{name: "SQL Injection"}, "MITIGATED_BY", "SecurityControl", %{name: "Input Sanitization"}) - create_rel("Vulnerability", %{name: "Cross-Site Scripting (XSS)"}, "MITIGATED_BY", "SecurityControl", %{name: "Output Encoding"}) + create_rel("Technology", %{name: "Streamlit"}, "HAS_BEST_PRACTICE", "BestPractice", %{ + name: "Streamlit Secret Management" + }) - create_rel("Technology", %{name: "Streamlit"}, "HAS_BEST_PRACTICE", "BestPractice", %{name: "Streamlit Secret Management"}) - create_rel("Technology", %{name: "Streamlit"}, "HAS_BEST_PRACTICE", "BestPractice", %{name: "Validate User Inputs in Forms"}) - create_rel("Technology", %{name: "SQLite"}, "HAS_BEST_PRACTICE", "BestPractice", %{name: "Use SQLAlchemy with Parameterized Queries"}) + create_rel("Technology", %{name: "Streamlit"}, "HAS_BEST_PRACTICE", "BestPractice", %{ + name: "Validate User Inputs in Forms" + }) - create_rel("BestPractice", %{name: "Use SQLAlchemy with Parameterized Queries"}, "IMPLEMENTS_CONTROL", "SecurityControl", %{name: "Parameterized Queries"}) - create_rel("BestPractice", %{name: "Validate User Inputs in Forms"}, "IMPLEMENTS_CONTROL", "SecurityControl", %{name: "Input Sanitization"}) + create_rel("Technology", %{name: "SQLite"}, "HAS_BEST_PRACTICE", "BestPractice", %{ + name: "Use SQLAlchemy with Parameterized Queries" + }) - create_rel("UseCase", %{name: "data_dashboard"}, "RECOMMENDED_DEPLOYMENT", "DeploymentPattern", %{name: "Streamlit Cloud"}) - create_rel("UseCase", %{name: "web_app_small_team"}, "RECOMMENDED_DEPLOYMENT", "DeploymentPattern", %{name: "Streamlit Cloud"}) - create_rel("UseCase", %{name: "web_app_small_team"}, "RECOMMENDED_DEPLOYMENT", "DeploymentPattern", %{name: "Fly.io Deployment"}) + create_rel( + "BestPractice", + %{name: "Use SQLAlchemy with Parameterized Queries"}, + "IMPLEMENTS_CONTROL", + "SecurityControl", + %{name: "Parameterized Queries"} + ) + + create_rel( + "BestPractice", + %{name: "Validate User Inputs in Forms"}, + "IMPLEMENTS_CONTROL", + "SecurityControl", + %{name: "Input Sanitization"} + ) + + create_rel( + "UseCase", + %{name: "data_dashboard"}, + "RECOMMENDED_DEPLOYMENT", + "DeploymentPattern", + %{name: "Streamlit Cloud"} + ) + + create_rel( + "UseCase", + %{name: "web_app_small_team"}, + "RECOMMENDED_DEPLOYMENT", + "DeploymentPattern", + %{name: "Streamlit Cloud"} + ) + + create_rel( + "UseCase", + %{name: "web_app_small_team"}, + "RECOMMENDED_DEPLOYMENT", + "DeploymentPattern", + %{name: "Fly.io Deployment"} + ) :ok end @@ -177,10 +263,14 @@ defmodule Guided.MCPServerTest do describe "Graph queries for tech_stack_recommendation" do test "can query technologies recommended for data_dashboard" do - {:ok, results} = Graph.query(""" - MATCH (t:Technology)-[:RECOMMENDED_FOR]->(uc:UseCase {name: 'data_dashboard'}) - RETURN t.name as technology, t.category as category - """) + {:ok, results} = + Graph.query(""" + MATCH (t:Technology)-[:RECOMMENDED_FOR]->(uc:UseCase {name: 'data_dashboard'}) + RETURN { + technology: t.name, + category: t.category + } + """) assert length(results) > 0 @@ -191,27 +281,37 @@ defmodule Guided.MCPServerTest do end test "can query vulnerabilities and mitigations" do - {:ok, results} = Graph.query(""" - MATCH (v:Vulnerability {name: 'SQL Injection'})-[:MITIGATED_BY]->(sc:SecurityControl) - RETURN v.name as vulnerability, sc.name as mitigation - """) + {:ok, results} = + Graph.query(""" + MATCH (v:Vulnerability {name: 'SQL Injection'})-[:MITIGATED_BY]->(sc:SecurityControl) + RETURN { + vulnerability: v.name, + mitigation: sc.name + } + """) assert length(results) > 0 # Should find Parameterized Queries as a mitigation - param_queries = Enum.find(results, fn r -> - r["mitigation"] == "Parameterized Queries" - end) + param_queries = + Enum.find(results, fn r -> + r["mitigation"] == "Parameterized Queries" + end) + assert param_queries != nil end end describe "Graph queries for secure_coding_pattern" do test "can query best practices for Streamlit" do - {:ok, results} = Graph.query(""" - MATCH (t:Technology {name: 'Streamlit'})-[:HAS_BEST_PRACTICE]->(bp:BestPractice) - RETURN bp.name as practice_name, bp.code_example as code_example - """) + {:ok, results} = + Graph.query(""" + MATCH (t:Technology {name: 'Streamlit'})-[:HAS_BEST_PRACTICE]->(bp:BestPractice) + RETURN { + practice_name: bp.name, + code_example: bp.code_example + } + """) assert length(results) > 0 @@ -222,10 +322,14 @@ defmodule Guided.MCPServerTest do end test "can query best practices with security controls" do - {:ok, results} = Graph.query(""" - MATCH (bp:BestPractice)-[:IMPLEMENTS_CONTROL]->(sc:SecurityControl) - RETURN bp.name as practice, sc.name as control - """) + {:ok, results} = + Graph.query(""" + MATCH (bp:BestPractice)-[:IMPLEMENTS_CONTROL]->(sc:SecurityControl) + RETURN { + practice: bp.name, + control: sc.name + } + """) assert length(results) > 0 end @@ -233,10 +337,15 @@ defmodule Guided.MCPServerTest do describe "Graph queries for deployment_guidance" do test "can query deployment patterns for use cases" do - {:ok, results} = Graph.query(""" - MATCH (uc:UseCase {name: 'data_dashboard'})-[:RECOMMENDED_DEPLOYMENT]->(dp:DeploymentPattern) - RETURN dp.name as pattern_name, dp.cost as cost, dp.https_support as https - """) + {:ok, results} = + Graph.query(""" + MATCH (uc:UseCase {name: 'data_dashboard'})-[:RECOMMENDED_DEPLOYMENT]->(dp:DeploymentPattern) + RETURN { + pattern_name: dp.name, + cost: dp.cost, + https: dp.https_support + } + """) assert length(results) > 0 @@ -244,15 +353,21 @@ defmodule Guided.MCPServerTest do pattern = List.first(results) assert pattern["pattern_name"] != nil assert pattern["cost"] != nil - assert is_boolean(pattern["https"]) + https_value = pattern["https"] + assert https_value in [true, false, "true", "false"] end test "deployment patterns have required attributes" do - {:ok, results} = Graph.query(""" - MATCH (dp:DeploymentPattern) - RETURN dp.name as name, dp.platform as platform, - dp.complexity as complexity, dp.cost as cost - """) + {:ok, results} = + Graph.query(""" + MATCH (dp:DeploymentPattern) + RETURN { + name: dp.name, + platform: dp.platform, + complexity: dp.complexity, + cost: dp.cost + } + """) assert length(results) > 0 @@ -266,11 +381,16 @@ defmodule Guided.MCPServerTest do describe "Graph data integrity" do test "all technologies have required attributes" do - {:ok, results} = Graph.query(""" - MATCH (t:Technology) - RETURN t.name as name, t.category as category, - t.description as description, t.security_rating as security_rating - """) + {:ok, results} = + Graph.query(""" + MATCH (t:Technology) + RETURN { + name: t.name, + category: t.category, + description: t.description, + security_rating: t.security_rating + } + """) assert length(results) >= 3 @@ -284,10 +404,14 @@ defmodule Guided.MCPServerTest do end test "all vulnerabilities have severity levels" do - {:ok, results} = Graph.query(""" - MATCH (v:Vulnerability) - RETURN v.name as name, v.severity as severity - """) + {:ok, results} = + Graph.query(""" + MATCH (v:Vulnerability) + RETURN { + name: v.name, + severity: v.severity + } + """) assert length(results) >= 2 @@ -297,10 +421,14 @@ defmodule Guided.MCPServerTest do end test "best practices have code examples" do - {:ok, results} = Graph.query(""" - MATCH (bp:BestPractice) - RETURN bp.name as name, bp.code_example as code_example - """) + {:ok, results} = + Graph.query(""" + MATCH (bp:BestPractice) + RETURN { + name: bp.name, + code_example: bp.code_example + } + """) assert length(results) >= 3 @@ -313,13 +441,18 @@ defmodule Guided.MCPServerTest do describe "Complex graph traversals" do test "can traverse from technology to vulnerabilities to mitigations" do - {:ok, results} = Graph.query(""" - MATCH (t:Technology {name: 'SQLite'}) - -[:HAS_VULNERABILITY]->(v:Vulnerability) - -[:MITIGATED_BY]->(sc:SecurityControl) - RETURN t.name as tech, v.name as vuln, - v.severity as severity, sc.name as mitigation - """) + {:ok, results} = + Graph.query(""" + MATCH (t:Technology {name: 'SQLite'}) + -[:HAS_VULNERABILITY]->(v:Vulnerability) + -[:MITIGATED_BY]->(sc:SecurityControl) + RETURN { + tech: t.name, + vuln: v.name, + severity: v.severity, + mitigation: sc.name + } + """) assert length(results) > 0 @@ -331,14 +464,88 @@ defmodule Guided.MCPServerTest do end test "can traverse use case to recommended tech to best practices" do - {:ok, results} = Graph.query(""" - MATCH (uc:UseCase {name: 'web_app_small_team'}) - <-[:RECOMMENDED_FOR]-(t:Technology) - -[:HAS_BEST_PRACTICE]->(bp:BestPractice) - RETURN uc.name as use_case, t.name as tech, bp.name as practice - """) + {:ok, results} = + Graph.query(""" + MATCH (uc:UseCase {name: 'web_app_small_team'}) + <-[:RECOMMENDED_FOR]-(t:Technology) + -[:HAS_BEST_PRACTICE]->(bp:BestPractice) + RETURN { + use_case: uc.name, + tech: t.name, + practice: bp.name + } + """) assert length(results) > 0 end end + + describe "MCP tool handlers" do + test "tech stack recommendation handles string-key params and JSON context" do + {:ok, frame} = Guided.MCPServer.init(%{}, Frame.new()) + + params = %{ + "intent" => "Build a data dashboard", + "context" => ~s({"users": "small_team"}) + } + + {:reply, response, _frame} = + Guided.MCPServer.handle_tool_call("tech_stack_recommendation", params, frame) + + result = response.structured_content + assert result[:status] == "success" + assert result[:use_case] == "data_dashboard" + end + + test "secure coding pattern handles string-key params" do + {:ok, frame} = Guided.MCPServer.init(%{}, Frame.new()) + + params = %{ + "technology" => "SQLite", + "task" => "database security" + } + + {:reply, response, _frame} = + Guided.MCPServer.handle_tool_call("secure_coding_pattern", params, frame) + + result = response.structured_content + assert result[:status] == "success" + assert result[:technology] == "SQLite" + assert result[:count] >= 1 + end + + test "deployment guidance accepts JSON-encoded stack lists" do + {:ok, frame} = Guided.MCPServer.init(%{}, Frame.new()) + + params = %{ + stack: ~s(["Streamlit", "SQLite"]) + } + + {:reply, response, _frame} = + Guided.MCPServer.handle_tool_call("deployment_guidance", params, frame) + + result = response.structured_content + assert result[:status] == "success" + + assert Enum.any?(result[:deployment_patterns], fn pattern -> + pattern[:name] == "Streamlit Cloud" + end) + end + + test "deployment guidance accepts comma-separated stacks" do + {:ok, frame} = Guided.MCPServer.init(%{}, Frame.new()) + + params = %{ + "stack" => "Streamlit, SQLite", + "requirements" => %{"budget" => "free"} + } + + {:reply, response, _frame} = + Guided.MCPServer.handle_tool_call("deployment_guidance", params, frame) + + result = response.structured_content + assert result[:status] == "success" + assert result[:recommendation][:name] in ["Streamlit Cloud", "Fly.io Deployment"] + end + end end diff --git a/guided/test/guided_web/mcp_integration_test.exs b/guided/test/guided_web/mcp_integration_test.exs new file mode 100644 index 0000000..520e98d --- /dev/null +++ b/guided/test/guided_web/mcp_integration_test.exs @@ -0,0 +1,207 @@ +defmodule GuidedWeb.MCPIntegrationTest do + use GuidedWeb.ConnCase, async: false + + alias Guided.Graph + + # Seed test data before each test + setup do + # Clear the graph + {:ok, _} = Graph.query("MATCH (n) DETACH DELETE n") + + # Seed minimal test data + seed_test_data() + + :ok + end + + defp seed_test_data do + # Create Technologies + {:ok, _} = Graph.create_node("Technology", %{ + name: "Streamlit", + category: "framework", + description: "Python framework for data apps", + security_rating: "good" + }) + + {:ok, _} = Graph.create_node("Technology", %{ + name: "SQLite", + category: "database", + description: "Lightweight SQL database", + security_rating: "good" + }) + + # Create Use Case + {:ok, _} = Graph.create_node("UseCase", %{ + name: "data_dashboard", + description: "Interactive data dashboard", + user_scale: "1-1000" + }) + + # Create Vulnerability + {:ok, _} = Graph.create_node("Vulnerability", %{ + name: "SQL Injection", + severity: "critical", + description: "Malicious SQL injection", + owasp_rank: "A03:2021" + }) + + # Create Security Control + {:ok, _} = Graph.create_node("SecurityControl", %{ + name: "Parameterized Queries", + category: "input_validation", + description: "Use prepared statements", + implementation_difficulty: "low" + }) + + # Create Best Practice + {:ok, _} = Graph.create_node("BestPractice", %{ + name: "Use SQLAlchemy with Parameterized Queries", + technology: "SQLite", + category: "database_security", + description: "Always use parameterized queries", + code_example: "session.query(User).filter(User.name == user_input)" + }) + + # Create Deployment Pattern + {:ok, _} = Graph.create_node("DeploymentPattern", %{ + name: "Streamlit Cloud", + platform: "streamlit_cloud", + cost: "free_tier_available", + complexity: "low", + description: "Official Streamlit hosting", + https_support: true + }) + + # Create Relationships + Graph.create_relationship("Technology", %{name: "Streamlit"}, "RECOMMENDED_FOR", "UseCase", %{name: "data_dashboard"}) + Graph.create_relationship("Technology", %{name: "SQLite"}, "RECOMMENDED_FOR", "UseCase", %{name: "data_dashboard"}) + Graph.create_relationship("Technology", %{name: "SQLite"}, "HAS_VULNERABILITY", "Vulnerability", %{name: "SQL Injection"}) + Graph.create_relationship("Vulnerability", %{name: "SQL Injection"}, "MITIGATED_BY", "SecurityControl", %{name: "Parameterized Queries"}) + Graph.create_relationship("Technology", %{name: "SQLite"}, "HAS_BEST_PRACTICE", "BestPractice", %{name: "Use SQLAlchemy with Parameterized Queries"}) + Graph.create_relationship("BestPractice", %{name: "Use SQLAlchemy with Parameterized Queries"}, "IMPLEMENTS_CONTROL", "SecurityControl", %{name: "Parameterized Queries"}) + Graph.create_relationship("UseCase", %{name: "data_dashboard"}, "RECOMMENDED_DEPLOYMENT", "DeploymentPattern", %{name: "Streamlit Cloud"}) + + :ok + end + + describe "MCP Server HTTP Integration" do + test "initialize creates a new session", %{conn: conn} do + conn = conn + |> put_req_header("content-type", "application/json") + |> put_req_header("accept", "application/json, text/event-stream") + |> post("/mcp", %{ + "jsonrpc" => "2.0", + "id" => 1, + "method" => "initialize", + "params" => %{ + "protocolVersion" => "2025-06-18", + "capabilities" => %{}, + "clientInfo" => %{"name" => "test", "version" => "1.0.0"} + } + }) + + assert conn.status == 200 + response = Jason.decode!(conn.resp_body) + assert response["result"]["serverInfo"]["name"] == "guided.dev MCP Server" + assert response["result"]["capabilities"]["tools"] == %{} + end + + test "tools/list returns all three tools", %{conn: conn} do + # First initialize + init_conn = conn + |> put_req_header("content-type", "application/json") + |> put_req_header("accept", "application/json, text/event-stream") + |> post("/mcp", %{ + "jsonrpc" => "2.0", + "id" => 1, + "method" => "initialize", + "params" => %{ + "protocolVersion" => "2025-06-18", + "capabilities" => %{}, + "clientInfo" => %{"name" => "test", "version" => "1.0.0"} + } + }) + + # Get session ID from init response + init_response = Jason.decode!(init_conn.resp_body) + assert init_response["result"] + + # Send notifications/initialized + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("accept", "application/json, text/event-stream") + |> post("/mcp", %{ + "jsonrpc" => "2.0", + "method" => "notifications/initialized" + }) + + # Now list tools + tools_conn = conn + |> put_req_header("content-type", "application/json") + |> put_req_header("accept", "application/json, text/event-stream") + |> post("/mcp", %{ + "jsonrpc" => "2.0", + "id" => 2, + "method" => "tools/list", + "params" => %{} + }) + + assert tools_conn.status == 200 + response = Jason.decode!(tools_conn.resp_body) + tools = response["result"]["tools"] + + assert length(tools) == 3 + tool_names = Enum.map(tools, & &1["name"]) + assert "tech_stack_recommendation" in tool_names + assert "secure_coding_pattern" in tool_names + assert "deployment_guidance" in tool_names + end + + test "tech_stack_recommendation tool returns recommendations", %{conn: conn} do + # Initialize session + init_conn = conn + |> put_req_header("content-type", "application/json") + |> put_req_header("accept", "application/json, text/event-stream") + |> post("/mcp", %{ + "jsonrpc" => "2.0", + "id" => 1, + "method" => "initialize", + "params" => %{ + "protocolVersion" => "2025-06-18", + "capabilities" => %{}, + "clientInfo" => %{"name" => "test", "version" => "1.0.0"} + } + }) + + assert init_conn.status == 200 + + # Send notifications/initialized + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("accept", "application/json, text/event-stream") + |> post("/mcp", %{ + "jsonrpc" => "2.0", + "method" => "notifications/initialized" + }) + + # Call tech_stack_recommendation + tool_conn = conn + |> put_req_header("content-type", "application/json") + |> put_req_header("accept", "application/json, text/event-stream") + |> post("/mcp", %{ + "jsonrpc" => "2.0", + "id" => 3, + "method" => "tools/call", + "params" => %{ + "name" => "tech_stack_recommendation", + "arguments" => %{ + "intent" => "build a dashboard for data visualization", + "context" => %{} + } + } + }) + + assert tool_conn.status in [200, 202] + end + end +end diff --git a/guided/test/manual/test_mcp_server.exs b/guided/test/manual/test_mcp_server.exs new file mode 100644 index 0000000..1e3d37b --- /dev/null +++ b/guided/test/manual/test_mcp_server.exs @@ -0,0 +1,103 @@ +#!/usr/bin/env elixir + +# Manual functional test for MCP server +# Run with: mix run test/manual/test_mcp_server.exs +# +# This test verifies the MCP server Cypher queries work correctly +# in the development environment where Apache AGE is available. + +alias Guided.Graph + +IO.puts("\n=== MCP Server Functional Test ===\n") + +# Test 1: tech_stack_recommendation query +IO.puts("Test 1: tech_stack_recommendation Cypher query") +IO.puts("Testing positional parameter with data_dashboard use case...") + +cypher_query = """ +MATCH (t:Technology)-[:RECOMMENDED_FOR]->(uc:UseCase {name: 'data_dashboard'}) +OPTIONAL MATCH (t)-[:HAS_VULNERABILITY]->(v:Vulnerability) +OPTIONAL MATCH (v)-[:MITIGATED_BY]->(sc:SecurityControl) +RETURN t.name as technology, + t.category as category, + t.description as description, + t.security_rating as security_rating, + v.name as vuln_name, + v.severity as vuln_severity, + v.description as vuln_description, + sc.name as mitigation_name +""" + +case Graph.query(cypher_query, []) do + {:ok, results} -> + IO.puts("✓ Query succeeded") + IO.puts(" Found #{length(results)} result rows") + if length(results) > 0 do + first = List.first(results) + IO.puts(" Sample: #{first["technology"] || "(no technology found)"}") + end + {:error, error} -> + IO.puts("✗ Query failed:") + IO.inspect(error, label: "Error") + System.halt(1) +end + +# Test 2: secure_coding_pattern query +IO.puts("\nTest 2: secure_coding_pattern Cypher query") +IO.puts("Testing with SQLite technology...") + +cypher_query2 = """ +MATCH (t:Technology {name: 'SQLite'})-[:HAS_BEST_PRACTICE]->(bp:BestPractice) +OPTIONAL MATCH (bp)-[:IMPLEMENTS_CONTROL]->(sc:SecurityControl) +RETURN bp.name as practice_name, + bp.category as category, + bp.description as description, + bp.code_example as code_example, + sc.name as security_control +""" + +case Graph.query(cypher_query2, []) do + {:ok, results} -> + IO.puts("✓ Query succeeded") + IO.puts(" Found #{length(results)} best practices") + if length(results) > 0 do + first = List.first(results) + IO.puts(" Sample: #{first["practice_name"] || "(no practice found)"}") + end + {:error, error} -> + IO.puts("✗ Query failed:") + IO.inspect(error, label: "Error") + System.halt(1) +end + +# Test 3: deployment_guidance query +IO.puts("\nTest 3: deployment_guidance Cypher query") +IO.puts("Testing with Streamlit and SQLite stack...") + +cypher_query3 = """ +MATCH (t:Technology)-[:RECOMMENDED_FOR]->(uc:UseCase)-[:RECOMMENDED_DEPLOYMENT]->(dp:DeploymentPattern) +WHERE t.name IN ['Streamlit', 'SQLite'] +RETURN dp.name as pattern_name, + dp.platform as platform, + dp.cost as cost, + dp.complexity as complexity, + dp.description as description, + dp.https_support as https_support, + uc.name as use_case +""" + +case Graph.query(cypher_query3, []) do + {:ok, results} -> + IO.puts("✓ Query succeeded") + IO.puts(" Found #{length(results)} deployment patterns") + if length(results) > 0 do + first = List.first(results) + IO.puts(" Sample: #{first["pattern_name"] || "(no pattern found)"}") + end + {:error, error} -> + IO.puts("✗ Query failed:") + IO.inspect(error, label: "Error") + System.halt(1) +end + +IO.puts("\n=== All tests passed! ===\n")