diff --git a/.gitignore b/.gitignore
index 505a3b1..e90a833 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@ wheels/
 
 # Virtual environments
 .venv
+.python-version
+uv.lock
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 43c4219..dabf107 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,13 +1,16 @@
 [project]
 name = "fs-mcp"
-version = "0.1.0"
+version = "0.1.1"
 description = "A secure MCP filesystem server with Stdio and Web UI modes."
-authors = [{name = "Your Name", email = "you@example.com"}]
+authors = [{name = "luutuankiet", email = "luutuankiet.ftu2@gmail.com"}]
 requires-python = ">=3.10"
 readme = "README.md"
 dependencies = [
     "fastmcp>=0.1.0",
+    "httpx>=0.28.1",
+    "pydantic>=2.0",
     "streamlit>=1.30.0",
+    "streamlit-js-eval>=0.1.5",
 ]
 
 [project.scripts]
@@ -23,4 +26,4 @@ packages = ["src/fs_mcp"]
 [dependency-groups]
 dev = [
     "pytest>=8.0.0",
-]
\ No newline at end of file
+]
diff --git a/src/fs_mcp/__main__.py b/src/fs_mcp/__main__.py
index 3c2b18d..cc90190 100644
--- a/src/fs_mcp/__main__.py
+++ b/src/fs_mcp/__main__.py
@@ -1,53 +1,102 @@
+# src/fs_mcp/__main__.py
+
 import argparse
 import sys
 import subprocess
+import time
 from pathlib import Path
-from fs_mcp import server
 
 def main():
-    parser = argparse.ArgumentParser(description="fs-mcp server")
-    parser.add_argument("--ui", action="store_true", help="Launch Web UI")
-    parser.add_argument("--host", default="0.0.0.0", help="UI Host")
-    parser.add_argument("--port", default="8501", help="UI Port")
-    parser.add_argument("dirs", nargs="*", help="Allowed directories")
+    parser = argparse.ArgumentParser(
+        description="fs-mcp server. By default, runs both UI and HTTP servers.",
+        formatter_class=argparse.RawTextHelpFormatter
+    )
+    # UI flags - now inverted to disable the default
+    ui_group = parser.add_argument_group('UI Options')
+    ui_group.add_argument("--no-ui", action="store_false", dest="run_ui", help="Do not launch the Streamlit Web UI.")
+    ui_group.add_argument("--host", default="0.0.0.0", help="Host for the Streamlit UI.")
+    ui_group.add_argument("--port", default="8123", type=int, help="Port for the Streamlit UI.")
     
-    # Parse known args to allow Streamlit to handle its own flags if needed
-    args, unknown = parser.parse_known_args()
+    # Background HTTP server flags - now inverted
+    http_group = parser.add_argument_group('HTTP Server Options')
+    http_group.add_argument("--no-http", action="store_false", dest="run_http", help="Do not run a background HTTP MCP server.")
+    http_group.add_argument("--http-host", default="0.0.0.0", help="Host for the background HTTP server.")
+    http_group.add_argument("--http-port", type=int, default=8124, help="Port for the background HTTP server.")
+
+    # Common args
+    parser.add_argument("dirs", nargs="*", help="Allowed directories (applies to all server modes).")
     
-    # Initialize Core Logic for Stdio mode
+    args, unknown = parser.parse_known_args()
     dirs = args.dirs or [str(Path.cwd())]
+
+    http_process = None
     try:
-        server.initialize(dirs)
-    except ValueError as e:
-        print(f"Error: {e}", file=sys.stderr)
-        sys.exit(1)
-
-    if args.ui:
-        # Launch Streamlit as a subprocess
-        # FIX: Find the file without importing it
-        current_dir = Path(__file__).parent
-        ui_path = (current_dir / "web_ui.py").resolve()
-        
-        if not ui_path.exists():
-            print(f"Error: Could not find web_ui.py at {ui_path}", file=sys.stderr)
-            sys.exit(1)
+        # --- Start Background HTTP Server if requested ---
+        if args.run_http:
+            # Command to run our dedicated HTTP runner script
+            http_cmd = [
+                sys.executable, "-m", "fs_mcp.http_runner",
+                "--host", args.http_host,
+                "--port", str(args.http_port),
+                *dirs
+            ]
+            print(f"🚀 Launching background HTTP MCP server process on http://{args.http_host}:{args.http_port}", file=sys.stderr)
+            
+            # Use Popen to start the process without blocking.
+            # We pipe stdout/stderr so they don't clutter the main console unless there's an error.
+            http_process = subprocess.Popen(http_cmd, stdout=sys.stderr, stderr=sys.stderr)
             
-        cmd = [
-            sys.executable, "-m", "streamlit", "run", str(ui_path),
-            "--server.address", args.host,
-            "--server.port", args.port,
-            "--",  # Separator: args after this are passed to the script
-            *dirs
-        ]
-        print(f"🚀 Launching UI on http://{args.host}:{args.port}", file=sys.stderr)
-        # Use simple run, Streamlit handles the rest
-        try:
-            subprocess.run(cmd)
-        except KeyboardInterrupt:
-            pass
-    else:
-        # Run Standard MCP Server
-        server.mcp.run()
+            # Give it a moment to start up and check for instant failure.
+            time.sleep(2) 
+            if http_process.poll() is not None:
+                print("❌ Background HTTP server failed to start. Check logs.", file=sys.stderr)
+                sys.exit(1)
+
+        # --- Start Foreground Application (UI or wait) ---
+        if args.run_ui:
+            current_dir = Path(__file__).parent
+            ui_path = (current_dir / "web_ui.py").resolve()
+            if not ui_path.exists():
+                raise FileNotFoundError(f"Could not find web_ui.py at {ui_path}")
+            
+            ui_cmd = [
+                sys.executable, "-m", "streamlit", "run", str(ui_path),
+                "--server.address", args.host,
+                "--server.port", str(args.port),
+                "--", *dirs
+            ]
+            print(f"🚀 Launching UI on http://{args.host}:{args.port}", file=sys.stderr)
+            # This is a blocking call. The script waits here until Streamlit exits.
+            subprocess.run(ui_cmd)
+
+        elif args.run_http:
+            # If ONLY the http server is running, we just need to wait.
+            print("Background HTTP server is running. Press Ctrl+C to stop.", file=sys.stderr)
+            http_process.wait()
+            
+        if not args.run_ui and not args.run_http:
+            # Default: run the original stdio server. This should be a direct import.
+            from fs_mcp import server
+            print("🚀 Launching Stdio MCP server", file=sys.stderr)
+            server.initialize(dirs)
+            server.mcp.run()
+
+    except KeyboardInterrupt:
+        print("\nCaught interrupt, shutting down...", file=sys.stderr)
+    
+    finally:
+        # This block is GUARANTEED to run, ensuring the background process is cleaned up.
+        if http_process:
+            print("Terminating background HTTP server...", file=sys.stderr)
+            http_process.terminate()  # Sends SIGTERM for a graceful shutdown
+            try:
+                # Wait up to 5 seconds for it to shut down
+                http_process.wait(timeout=5)
+                print("Background server stopped gracefully.", file=sys.stderr)
+            except subprocess.TimeoutExpired:
+                # If it's stuck, force kill it.
+                print("Server did not terminate gracefully, killing.", file=sys.stderr)
+                http_process.kill()
 
 if __name__ == "__main__":
     main()
\ No newline at end of file
diff --git a/src/fs_mcp/http_runner.py b/src/fs_mcp/http_runner.py
new file mode 100644
index 0000000..fa4295f
--- /dev/null
+++ b/src/fs_mcp/http_runner.py
@@ -0,0 +1,28 @@
+import argparse
+from fs_mcp import server
+
+def main():
+    """
+    This is a dedicated entry point for running the FastMCP server in HTTP mode.
+    It's designed to be called as a subprocess from the main CLI.
+    """
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--host", required=True)
+    parser.add_argument("--port", type=int, required=True)
+    parser.add_argument("dirs", nargs="*")
+    args = parser.parse_args()
+    
+    try:
+        server.initialize(args.dirs)
+        server.mcp.run(
+            transport="streamable-http",
+            host=args.host,
+            port=args.port
+        )
+    except KeyboardInterrupt:
+        pass # The main process will handle termination.
+    except Exception as e:
+        print(f"HTTP runner failed: {e}")
+
+if __name__ == "__main__":
+    main()
\ No newline at end of file
diff --git a/src/fs_mcp/server.py b/src/fs_mcp/server.py
index 019d34e..580d8c4 100644
--- a/src/fs_mcp/server.py
+++ b/src/fs_mcp/server.py
@@ -1,3 +1,5 @@
+import json
+from pydantic import BaseModel
 import os
 import base64
 import mimetypes
@@ -7,10 +9,22 @@ from typing import List, Optional, Literal, Dict
 from datetime import datetime
 from fastmcp import FastMCP
 
+from dataclasses import dataclass
+import difflib
+
+# The new structure for returning detailed results from the edit tool.
+@dataclass
+class EditResult:
+    success: bool
+    message: str
+    diff: Optional[str] = None
+    error_type: Optional[str] = None
+
 # --- Global Configuration ---
 ALLOWED_DIRS: List[Path] = []
 mcp = FastMCP("filesystem")
 
+
 def initialize(directories: List[str]):
     """Initialize the allowed directories configuration."""
     global ALLOWED_DIRS
@@ -184,17 +198,15 @@ def move_file(source: str, destination: str) -> str:
     return f"Moved {source} to {destination}"
 
 @mcp.tool()
-def search_files(path: str, pattern: str, exclude_patterns: List[str] = []) -> str:
-    """Recursively search for files matching glob pattern."""
+def search_files(path: str, pattern: str) -> str:
+    """Recursively search for files matching a glob pattern."""
     root = validate_path(path)
-    results = []
-    for r, d, f in os.walk(root):
-        d[:] = [x for x in d if not any(fnmatch.fnmatch(x, p) for p in exclude_patterns)]
-        for name in f:
-            if any(fnmatch.fnmatch(name, p) for p in exclude_patterns): continue
-            if fnmatch.fnmatch(name, pattern):
-                results.append(str(Path(r) / name))
-    return "\n".join(results) or "No matches found"
+    try:
+        results = [str(p.relative_to(root)) for p in root.rglob(pattern) if p.is_file()]
+        return "\n".join(results) or "No matches found."
+    except Exception as e:
+        return f"Error during search: {e}"
+
 
 @mcp.tool()
 def get_file_info(path: str) -> str:
@@ -204,36 +216,152 @@ def get_file_info(path: str) -> str:
     return f"Path: {p}\nType: {'Dir' if p.is_dir() else 'File'}\nSize: {format_size(s.st_size)}\nModified: {datetime.fromtimestamp(s.st_mtime)}"
 
 @mcp.tool()
-def directory_tree(path: str, exclude_patterns: List[str] = []) -> str:
-    """Get recursive JSON tree."""
-    import json
+def directory_tree(path: str, max_depth: int = 3, exclude_dirs: Optional[List[str]] = None) -> str:
+    """Get recursive JSON tree with depth limit and default excludes."""
     root = validate_path(path)
-    def build(current: Path) -> Dict:
-        name = current.name or str(current)
-        if any(fnmatch.fnmatch(name, p) for p in exclude_patterns): return None
-        node = {"name": name, "type": "directory" if current.is_dir() else "file"}
+    
+    # Use provided excludes or our new smart defaults
+    default_excludes = ['.git', '.venv', '__pycache__', 'node_modules', '.pytest_cache']
+    excluded = exclude_dirs if exclude_dirs is not None else default_excludes
+
+    def build(current: Path, depth: int) -> Optional[Dict]:
+        if depth > max_depth or current.name in excluded:
+            return None
+        
+        node = {"name": current.name, "type": "directory" if current.is_dir() else "file"}
+        
         if current.is_dir():
-            node["children"] = [c for c in [build(e) for e in sorted(current.iterdir(), key=lambda x: x.name)] if c]
+            children = []
+            try:
+                for entry in sorted(current.iterdir(), key=lambda x: x.name):
+                    child = build(entry, depth + 1)
+                    if child:
+                        children.append(child)
+                if children:
+                    node["children"] = children
+            except PermissionError:
+                node["error"] = "Permission Denied"
         return node
-    return json.dumps(build(root), indent=2)
+        
+    tree = build(root, 0)
+    return json.dumps(tree, indent=2)
+
+class RooStyleEditTool:
+    """
+    A robust, agent-friendly file editing tool that validates operations
+    before making changes to prevent common errors.
+    """
+    def count_occurrences(self, content: str, substr: str) -> int:
+        if substr == "": return 0
+        return content.count(substr)
+
+    def normalize_line_endings(self, content: str) -> str:
+        return content.replace('\r\n', '\n').replace('\r', '\n')
+    
+    def edit_file(self, file_path: str, old_string: str, new_string: str, 
+                  expected_replacements: int = 1, dry_run: bool = False) -> EditResult:
+        
+        p = validate_path(file_path)
+        
+        file_exists = p.exists()
+        is_new_file = not file_exists and old_string == ""
+
+        if not file_exists and not is_new_file:
+            return EditResult(success=False, message=f"File not found: {file_path}. To create a new file, old_string must be empty.", error_type="file_not_found")
+        
+        if file_exists and is_new_file:
+            return EditResult(success=False, message=f"File '{file_path}' already exists. Cannot create a new file when one already exists.", error_type="file_exists")
+        
+        original_content = ""
+        if file_exists:
+            try:
+                original_content = p.read_text(encoding='utf-8')
+            except Exception as e:
+                return EditResult(success=False, message=f"Failed to read file: {e}", error_type="read_error")
+
+        normalized_content = self.normalize_line_endings(original_content)
+        normalized_old = self.normalize_line_endings(old_string)
+        
+        if not is_new_file:
+            if old_string == new_string:
+                return EditResult(success=False, message="No changes to apply. The old_string and new_string are identical.", error_type="validation_error")
+            
+            occurrences = self.count_occurrences(normalized_content, normalized_old)
+            
+            if occurrences == 0:
+                return EditResult(success=False, message="No match found for the specified 'old_string'. Please ensure it matches exactly.", error_type="validation_error")
+            
+            if occurrences != expected_replacements:
+                return EditResult(success=False, message=f"Expected {expected_replacements} occurrence(s) but found {occurrences}. Please adjust your 'old_string' or 'expected_replacements' value.", error_type="validation_error")
+        
+        if is_new_file:
+            new_content = new_string
+        else:
+            # Note: This is a global replace, not line-by-line.
+            new_content = normalized_content.replace(normalized_old, new_string)
+        
+        diff_str = "\n".join(difflib.unified_diff(
+            original_content.splitlines(), new_content.splitlines(), 
+            fromfile=f"a/{file_path}", tofile=f"b/{file_path}", lineterm=""
+        ))
+        
+        if not dry_run:
+            try:
+                p.parent.mkdir(parents=True, exist_ok=True)
+                p.write_text(new_content, encoding='utf-8')
+            except Exception as e:
+                return EditResult(success=False, message=f"Failed to write to file: {e}", error_type="write_error")
+
+        return EditResult(success=True, message=f"Successfully edited '{file_path}'.", diff=diff_str)
+
+@mcp.tool()
+def edit_file(path: str, old_string: str, new_string: str, 
+              expected_replacements: int = 1, dry_run: bool = False) -> str:
+    """
+    [UPGRADED] A robust tool for editing files. It can replace text, create new files,
+    and provides detailed, agent-friendly error messages to prevent mistakes.
+    - To create a new file, set `old_string` to "" and provide the full content in `new_string`.
+    - To replace text, provide the exact `old_string` to be replaced.
+    - `expected_replacements` ensures you don't accidentally edit more lines than intended.
+    """
+    tool = RooStyleEditTool()
+    result = tool.edit_file(path, old_string, new_string, expected_replacements, dry_run)
+    
+    if result.success:
+        output = result.message
+        if result.diff:
+            output += f"\n\n--- DIFF ---\n{result.diff}"
+        return output
+    else:
+        # Raising an exception is the correct way to signal a tool error.
+        raise ValueError(f"Edit failed: {result.message} (Error type: {result.error_type})")
+
+
+@mcp.tool()
+def grounding_search(query: str) -> str:
+    """[NEW] A custom search tool. Accepts a natural language query and returns a grounded response."""
+    # This is a placeholder for a future RAG or other search implementation.
+    print(f"Received grounding search query: {query}")
+    return "DEVELOPER PLEASE UPDATE THIS WITH ACTUAL CONTENT"
+
 
 @mcp.tool()
-def edit_file(path: str, edits: List[Dict[str, str]], dry_run: bool = False) -> str:
-    """Line-based file editing with diff preview."""
-    import difflib
+def append_text(path: str, content: str) -> str:
+    """
+    Append text to the end of a file. 
+    Use this as a fallback if edit_file fails to find a match.
+    """
     p = validate_path(path)
-    with open(p, 'r', encoding='utf-8') as f: original = f.read()
-    modified = original
-    for edit in edits:
-        old = edit['oldText'].replace('\r\n', '\n')
-        new = edit['newText'].replace('\r\n', '\n')
-        if old not in modified: raise ValueError(f"Text not found: {old}")
-        modified = modified.replace(old, new, 1)
+    if not p.exists():
+        raise FileNotFoundError(f"File not found: {path}. Cannot append to a non-existent file.")
     
-    diff = "\n".join(difflib.unified_diff(
-        original.splitlines(), modified.splitlines(), 
-        fromfile="original", tofile="modified", lineterm=""
-    ))
-    if not dry_run:
-        with open(p, 'w', encoding='utf-8') as f: f.write(modified)
-    return diff
\ No newline at end of file
+    # Ensure there is a newline at the start of the append if the file doesn't have one
+    # to avoid clashing with the existing last line.
+    with open(p, 'a', encoding='utf-8') as f:
+        # Check if we need a leading newline
+        if p.stat().st_size > 0:
+            f.write("\n")
+        f.write(content)
+        
+    return f"Successfully appended content to '{path}'."
+
diff --git a/src/fs_mcp/web_ui.py b/src/fs_mcp/web_ui.py
index 99ef95f..45cb1d0 100644
--- a/src/fs_mcp/web_ui.py
+++ b/src/fs_mcp/web_ui.py
@@ -3,8 +3,12 @@ import sys
 import inspect
 import json
 import base64
+import asyncio
 from typing import Optional, Union, List, Dict, Any
 from pathlib import Path
+from dataclasses import asdict
+from fastmcp.utilities.inspect import inspect_fastmcp
+from streamlit_js_eval import streamlit_js_eval
 
 # --- 1. SETUP & CONFIG ---
 st.set_page_config(page_title="FS-MCP", layout="wide", page_icon="📂")
@@ -35,37 +39,34 @@ st.sidebar.header("Active Configuration")
 st.sidebar.code("\n".join(str(d) for d in server.ALLOWED_DIRS))
 
 # --- 3. TOOL DISCOVERY & SCHEMA EXPORT ---
-KNOWN_TOOLS = [
-    "list_directory", "list_directory_with_sizes", "read_text_file", "read_multiple_files",
-    "read_media_file", "write_file", "create_directory", 
-    "move_file", "search_files", "get_file_info", 
-    "directory_tree", "edit_file", "list_allowed_directories"
-]
-
 tools = {}
 tool_schemas = []
 
-for name in KNOWN_TOOLS:
-    if hasattr(server, name):
-        wrapper = getattr(server, name)
-        fn = wrapper.fn if hasattr(wrapper, 'fn') else wrapper
-        tools[name] = fn
-        
-        # Build Schema for export
-        schema = {
-            "name": name,
-            "description": inspect.getdoc(fn) or "",
-            "inputSchema": {"type": "object", "properties": {}}
-        }
-        sig = inspect.signature(fn)
-        for param_name, param in sig.parameters.items():
-            if param_name in ['ctx', 'context']: continue
-            param_type = "string"
-            if param.annotation == int: param_type = "integer"
-            if param.annotation == bool: param_type = "boolean"
-            if param.annotation == list: param_type = "array"
-            schema["inputSchema"]["properties"][param_name] = {"type": param_type}
-        tool_schemas.append(schema)
+try:
+    # 1. Use the official inspect utility to get a structured server blueprint
+    server_info = asyncio.run(inspect_fastmcp(server.mcp))
+
+    # 2. Convert the ToolInfo dataclasses to dictionaries using asdict()
+    tool_schemas = [asdict(tool) for tool in server_info.tools]
+
+    # 3. Map the functions for the UI to execute
+    # Create a name-to-schema mapping for easier lookup
+    schema_map = {schema['name']: schema for schema in tool_schemas}
+    
+    for tool_info in server_info.tools:
+        name = tool_info.name
+        if hasattr(server, name):
+            wrapper = getattr(server, name)
+            # Unwrap FastMCP decorators if needed
+            fn = wrapper.fn if hasattr(wrapper, 'fn') else wrapper
+            tools[name] = fn
+        else:
+            st.warning(f"Tool '{name}' has a schema but no matching function found in server.py")
+
+except Exception as e:
+    st.error(f"Failed to inspect MCP server: {e}")
+    st.exception(e) 
+    st.stop()
 
 with st.sidebar.expander("🔌 Tool Schemas (JSON)", expanded=False):
     st.caption("Copy this to agent configuration:")
@@ -125,7 +126,7 @@ if not tools:
     st.error("No tools found.")
     st.stop()
 
-selected = st.sidebar.radio("Available Tools", list(tools.keys()))
+selected = st.sidebar.radio("Available Tools", sorted(tools.keys()))
 fn = tools[selected]
 sig = inspect.signature(fn)
 
@@ -134,7 +135,7 @@ if inspect.getdoc(fn):
     st.info(inspect.getdoc(fn))
 
 # INPUT TABS
-tab_form, tab_raw, tab_compact = st.tabs(["📝 Interactive Form", "📄 Raw JSON", "⚡ Compact JSON"])
+tab_raw, tab_compact, tab_form = st.tabs(["📄 Raw JSON", "⚡ Compact JSON", "📝 Interactive Form"])
 
 execution_args = None
 trigger_run = False
@@ -152,7 +153,7 @@ with tab_form:
             is_bool = (annotation == bool) or (getattr(annotation, "__origin__", None) is Union and bool in getattr(annotation, "__args__", []))
             
             if name in ['path', 'source', 'destination']:
-                def_val = str(server.ALLOWED_DIRS[0])
+                def_val = str(server.ALLOWED_DIRS[0]) if server.ALLOWED_DIRS else ""
                 form_inputs[name] = st.text_input(name, value=def_val)
             elif name == 'content':
                 st.caption("Literal Content (WYSIWYG - Enter creates newlines)")
@@ -197,9 +198,10 @@ with tab_form:
 default_args = {}
 for name, param in sig.parameters.items():
     if name in ['ctx', 'context']: continue
-    if name in ['path', 'source', 'destination']: default_args[name] = str(server.ALLOWED_DIRS[0])
+    if name in ['path', 'source', 'destination']:
+        default_args[name] = str(server.ALLOWED_DIRS[0]) if server.ALLOWED_DIRS else ""
     elif name == 'content': default_args[name] = "Line 1\nLine 2"
-    elif name == 'paths': default_args[name] = [str(server.ALLOWED_DIRS[0])]
+    elif name == 'paths': default_args[name] = [str(server.ALLOWED_DIRS[0])] if server.ALLOWED_DIRS else []
     else: default_args[name] = ""
 
 json_template = json.dumps(default_args, indent=2)
@@ -228,17 +230,19 @@ with tab_compact:
 if trigger_run and execution_args is not None:
     st.divider()
     
-    # 1. Run
     with st.spinner("Running tool..."):
         res_raw, res_proto, dtype, err = execute_tool(fn, execution_args)
 
-    # 2. Display Status
     if err:
         st.error("Tool Execution Failed")
     else:
         st.success("Tool Execution Successful")
+        # --- AUTO-COPY LOGIC ---
+        json_response = json.dumps(res_proto, indent=None)
+        escaped_json = json.dumps(json_response)
+        streamlit_js_eval(js_expressions=f"navigator.clipboard.writeText({escaped_json})")
+        st.toast("🤖 Agent response copied to clipboard!")
 
-    # 3. Split View: Human vs Agent
     col_human, col_agent = st.columns(2)
     
     with col_human:
@@ -250,7 +254,6 @@ if trigger_run and execution_args is not None:
         elif dtype == "json":
             st.json(res_raw)
         else:
-            # Check for diffs
             text = str(res_raw)
             if text.startswith("---") or text.startswith("+++"):
                 st.code(text, language="diff")
