{"id":1026,"date":"2026-10-30T15:10:35","date_gmt":"2026-10-30T06:10:35","guid":{"rendered":"https:\/\/csharp.ihavenomoney.co.kr\/?p=1026"},"modified":"2026-10-30T15:10:35","modified_gmt":"2026-10-30T06:10:35","slug":"ubuntu-mysql-chatgpt-python-mcp-guide","status":"publish","type":"post","link":"https:\/\/csharp.ihavenomoney.co.kr\/?p=1026","title":{"rendered":"Ubuntu MySQL\uc744 ChatGPT\uc5d0 \uc5f0\uacb0\ud558\uae30: Python MCP \uc11c\ubc84 \uc124\uce58\ubd80\ud130 \uc870\ud68c\u00b7\uc218\uc815\u00b7SSH \uc2b9\uc778\uae4c\uc9c0"},"content":{"rendered":"<p><strong>\ub300\uc0c1:<\/strong> Ubuntu 24.04 LTS \/ MySQL 8.x \/ Python 3.12 \/ Python MCP SDK v2. \uc870\ud68c\ubfd0 \uc544\ub2c8\ub77c INSERT\u00b7UPDATE\u00b7DELETE\ub97c \uc9c0\uc6d0\ud558\ub418, \ubaa8\ub4e0 \ubcc0\uacbd\uc740 \uc11c\ubc84 \uad00\ub9ac\uc790 SSH \uc2b9\uc778 \uc774\ud6c4\uc5d0\ub9cc \uc2e4\ud589\ud569\ub2c8\ub2e4. <strong>\uc6b4\uc601 DB\uc5d0 \ubc14\ub85c \uc801\uc6a9\ud558\uc9c0 \ub9d0\uace0 \ud14c\uc2a4\ud2b8 DB\uc5d0\uc11c \ucda9\ubd84\ud788 \uac80\uc99d\ud558\uc138\uc694.<\/strong><\/p>\n<h2>1. \uad6c\uc131\uacfc \ubcf4\uc548 \uc6d0\uce59<\/h2>\n<p>ChatGPT \u2192 Secure MCP Tunnel(HTTPS 443 \uc544\uc6c3\ubc14\uc6b4\ub4dc) \u2192 Ubuntu\uc758 Python MCP(127.0.0.1:8000\/mcp) \u2192 MySQL(127.0.0.1:3306). MySQL \ud3ec\ud2b8\ub294 \uc678\ubd80\uc5d0 \uacf5\uac1c\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud14c\uc774\ube14\u00b7\uceec\ub7fc \ud5c8\uc6a9 \ubaa9\ub85d, \ub2e8\uc77c \ud589 \ubcc0\uacbd, 5\ubd84 \ub9cc\ub8cc \uc2b9\uc778, \ud2b8\ub79c\uc7ad\uc158, \uc911\ubcf5 \uc2e4\ud589 \ubc29\uc9c0 \ub85c\uadf8\ub97c \uc801\uc6a9\ud569\ub2c8\ub2e4.<\/p>\n<h2>2. Ubuntu \uc124\uce58<\/h2>\n<pre class=\"wp-block-code\"><code class=\"language-bash\">sudo apt update\nsudo apt install -y python3 python3-venv python3-pip default-mysql-client curl ca-certificates\nsudo install -d -m 755 \/opt\/mysql-mcp\nsudo chown &quot;$USER&quot;:&quot;$USER&quot; \/opt\/mysql-mcp\ncd \/opt\/mysql-mcp\npython3 -m venv .venv\n.venv\/bin\/python -m pip install --upgrade pip\n.venv\/bin\/pip install &quot;mcp&gt;=2,&lt;3&quot; mysql-connector-python\n.venv\/bin\/pip freeze &gt; requirements.lock.txt<\/code><\/pre>\n<h2>3. \ud14c\uc2a4\ud2b8 MySQL \uc2a4\ud0a4\ub9c8 \ubc0f \uad8c\ud55c<\/h2>\n<p>MySQL \uad00\ub9ac\uc790\uc5d0\uc11c \uc544\ub798 SQL\uc744 \uc2e4\ud589\ud569\ub2c8\ub2e4. \uc0d8\ud50c DB\uc758 \uacc4\uc815 \ube44\ubc00\ubc88\ud638\ub294 \ubc18\ub4dc\uc2dc \ubcc0\uacbd\ud558\uace0, \uc6b4\uc601 \ud14c\uc774\ube14\uc5d0\ub294 \ud14c\uc774\ube14 \ub2e8\uc704 GRANT\ub97c \ub530\ub85c \uc124\uc815\ud569\ub2c8\ub2e4.<\/p>\n<pre class=\"wp-block-code\"><code class=\"language-sql\">CREATE DATABASE IF NOT EXISTS sampledb CHARACTER SET utf8mb4;\nUSE sampledb;\nCREATE TABLE IF NOT EXISTS mcp_demo_items (\n id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,\n name VARCHAR(100) NOT NULL,\n price DECIMAL(12,2) NOT NULL,\n stock INT NOT NULL DEFAULT 0\n) ENGINE=InnoDB;\nCREATE TABLE IF NOT EXISTS mcp_change_log (\n request_id CHAR(32) PRIMARY KEY,\n action VARCHAR(10) NOT NULL,\n table_name VARCHAR(64) NOT NULL,\n target_id BIGINT UNSIGNED NULL,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n) ENGINE=InnoDB;\nINSERT INTO mcp_demo_items(name, price, stock)\nVALUES ('\ud0a4\ubcf4\ub4dc',35000,10),('\ub9c8\uc6b0\uc2a4',15000,20);\nCREATE USER 'mcp_app'@'127.0.0.1' IDENTIFIED BY 'CHANGE_TO_STRONG_PASSWORD';\nGRANT SELECT,INSERT,UPDATE,DELETE ON sampledb.mcp_demo_items TO 'mcp_app'@'127.0.0.1';\nGRANT SELECT,INSERT ON sampledb.mcp_change_log TO 'mcp_app'@'127.0.0.1';\nSHOW GRANTS FOR 'mcp_app'@'127.0.0.1';<\/code><\/pre>\n<h2>4. \ud5c8\uc6a9 \ud14c\uc774\ube14\u00b7\uceec\ub7fc \uc815\ucc45<\/h2>\n<p>\ud30c\uc77c <code>\/opt\/mysql-mcp\/policy.json<\/code>\uc744 \uc0dd\uc131\ud569\ub2c8\ub2e4. \uc0ad\uc81c\uac00 \ubd88\ud544\uc694\ud558\ub2e4\uba74 delete\ub97c false\ub85c \uc720\uc9c0\ud569\ub2c8\ub2e4. \uc815\uc218\ud615 \uc790\ub3d9\uc99d\uac00 \uae30\ubcf8\ud0a4, \ub2e8\uc77c \ud589 \uc791\uc5c5\ub9cc \uc9c0\uc6d0\ud558\ub294 \uc0d8\ud50c\uc785\ub2c8\ub2e4.<\/p>\n<pre class=\"wp-block-code\"><code class=\"language-json\">{\n &quot;mcp_demo_items&quot;: {\n  &quot;pk&quot;: &quot;id&quot;,\n  &quot;read&quot;: [&quot;id&quot;,&quot;name&quot;,&quot;price&quot;,&quot;stock&quot;],\n  &quot;write&quot;: [&quot;name&quot;,&quot;price&quot;,&quot;stock&quot;],\n  &quot;delete&quot;: false\n }\n}<\/code><\/pre>\n<h2>5. \uc11c\ubc84 \ucf54\ub4dc: server.py<\/h2>\n<p><code>\/opt\/mysql-mcp\/server.py<\/code>\uc5d0 \uc544\ub798 \ucf54\ub4dc\ub97c \uc800\uc7a5\ud569\ub2c8\ub2e4. <code>list_tables<\/code>, <code>describe_table<\/code>, <code>list_rows<\/code>, <code>get_row<\/code>, <code>prepare_change<\/code>, <code>apply_change<\/code>\uc758 6\uac1c \ub3c4\uad6c\ub97c \uc81c\uacf5\ud569\ub2c8\ub2e4.<\/p>\n<pre class=\"wp-block-code\"><code class=\"language-python\">import os, re, json, uuid, fcntl\nfrom pathlib import Path\nfrom datetime import datetime, timezone, timedelta\nimport mysql.connector\nfrom mcp.server import MCPServer\nfrom mcp.types import ToolAnnotations\n\n# Only localhost exposure; tunnel handles external transport.\nmcp = MCPServer(&quot;Ubuntu-MySQL&quot;)\nPOLICY = json.loads(Path(&quot;\/opt\/mysql-mcp\/policy.json&quot;).read_text())\nPENDING = Path(&quot;\/var\/lib\/mysql-mcp\/pending&quot;)\nIDENT = re.compile(r&quot;^[A-Za-z_][A-Za-z0-9_]{0,63}$&quot;)\n\ndef db():\n    return mysql.connector.connect(\n        host=&quot;127.0.0.1&quot;, port=3306,\n        user=os.environ[&quot;MYSQL_USER&quot;],\n        password=os.environ[&quot;MYSQL_PASSWORD&quot;],\n        database=os.environ[&quot;MYSQL_DATABASE&quot;],\n        autocommit=False, connection_timeout=5)\n\ndef quoted(name):\n    if not IDENT.fullmatch(name): raise ValueError(&quot;Bad identifier&quot;)\n    return &quot;`&quot; + name + &quot;`&quot;\n\ndef policy(table):\n    p = POLICY.get(table)\n    if not p: raise ValueError(&quot;Table not allowed&quot;)\n    for c in [table, p[&quot;pk&quot;], *p[&quot;read&quot;], *p[&quot;write&quot;]]:\n        quoted(c)\n    if not set(p[&quot;write&quot;]).issubset(set(p[&quot;read&quot;]) - {p[&quot;pk&quot;]}):\n        raise ValueError(&quot;Invalid policy&quot;)\n    return p\n\ndef norm(obj):\n    return json.loads(json.dumps(obj, default=str, ensure_ascii=False))\n\ndef one(conn, table, row_id, lock=False):\n    p = policy(table)\n    cols = &quot;,&quot;.join(map(quoted, p[&quot;read&quot;]))\n    sql = (f&quot;SELECT {cols} FROM {quoted(table)} &quot;\n           f&quot;WHERE {quoted(p['pk'])}=%s LIMIT 1&quot; +\n           (&quot; FOR UPDATE&quot; if lock else &quot;&quot;))\n    cur = conn.cursor(dictionary=True, buffered=True)\n    try:\n        cur.execute(sql, (row_id,))\n        return norm(cur.fetchone())\n    finally:\n        cur.close()\n\ndef valid_values(table, action, values):\n    p = policy(table)\n    if action == &quot;delete&quot;:\n        if values: raise ValueError(&quot;DELETE has no values&quot;)\n        return {}\n    if not isinstance(values, dict) or not values:\n        raise ValueError(&quot;Nonempty values required&quot;)\n    if not set(values).issubset(p[&quot;write&quot;]):\n        raise ValueError(&quot;Column not allowed&quot;)\n    for v in values.values():\n        if v is not None and type(v) not in (str, int, float, bool):\n            raise ValueError(&quot;Only scalar values&quot;)\n        if isinstance(v, str) and len(v) &gt; 200:\n            raise ValueError(&quot;Value too long&quot;)\n    return values\n\ndef request_path(rid):\n    if not re.fullmatch(r&quot;[a-f0-9]{32}&quot;, rid):\n        raise ValueError(&quot;Invalid request ID&quot;)\n    return PENDING \/ (rid + &quot;.json&quot;)\n\n@mcp.tool(annotations=ToolAnnotations(read_only_hint=True))\ndef list_tables() -&gt; dict:\n    &quot;&quot;&quot;List approved tables.&quot;&quot;&quot;\n    return {&quot;tables&quot;: list(POLICY)}\n\n@mcp.tool(annotations=ToolAnnotations(read_only_hint=True))\ndef describe_table(table: str) -&gt; dict:\n    &quot;&quot;&quot;Describe allowed fields, primary key, write policy.&quot;&quot;&quot;\n    return policy(table)\n\n@mcp.tool(annotations=ToolAnnotations(read_only_hint=True))\ndef list_rows(table: str, limit: int = 10) -&gt; dict:\n    &quot;&quot;&quot;Read up to 10 approved-column rows.&quot;&quot;&quot;\n    p = policy(table)\n    columns = &quot;,&quot;.join(map(quoted, p[&quot;read&quot;]))\n    sql = (f&quot;SELECT {columns} FROM {quoted(table)} &quot;\n           f&quot;ORDER BY {quoted(p['pk'])} DESC LIMIT %s&quot;)\n    conn = db()\n    try:\n        cur = conn.cursor(dictionary=True, buffered=True)\n        try:\n            cur.execute(sql, (max(1, min(limit, 10)),))\n            return {&quot;rows&quot;: norm(cur.fetchall())}\n        finally:\n            cur.close()\n    finally:\n        conn.close()\n\n@mcp.tool(annotations=ToolAnnotations(read_only_hint=True))\ndef get_row(table: str, row_id: int) -&gt; dict:\n    &quot;&quot;&quot;Read one row by positive integer primary key.&quot;&quot;&quot;\n    if row_id &lt; 1: raise ValueError(&quot;Invalid row_id&quot;)\n    conn = db()\n    try:\n        return {&quot;row&quot;: one(conn, table, row_id)}\n    finally:\n        conn.close()\n\n@mcp.tool()\ndef prepare_change(action: str, table: str,\n                   row_id: int | None = None,\n                   values: dict | None = None) -&gt; dict:\n    &quot;&quot;&quot;Preview one INSERT, UPDATE, or DELETE. No data writes yet.&quot;&quot;&quot;\n    if action not in (&quot;insert&quot;, &quot;update&quot;, &quot;delete&quot;):\n        raise ValueError(&quot;Unsupported action&quot;)\n    p = policy(table)\n    if action == &quot;delete&quot; and not p.get(&quot;delete&quot;, False):\n        raise ValueError(&quot;DELETE disabled&quot;)\n    if action == &quot;insert&quot; and row_id is not None:\n        raise ValueError(&quot;INSERT ID must auto-increment&quot;)\n    if action != &quot;insert&quot; and (row_id is None or row_id &lt; 1):\n        raise ValueError(&quot;Positive row_id required&quot;)\n    values = valid_values(table, action, values)\n    before = None\n    if action != &quot;insert&quot;:\n        conn = db()\n        try:\n            before = one(conn, table, row_id)\n        finally:\n            conn.close()\n        if before is None: raise ValueError(&quot;Row missing&quot;)\n    rid = uuid.uuid4().hex\n    item = dict(request_id=rid, status=&quot;pending&quot;,\n        approved=False, action=action, table=table,\n        row_id=row_id, values=values, before=before,\n        expires=(datetime.now(timezone.utc) +\n                 timedelta(minutes=5)).isoformat())\n    fd = os.open(request_path(rid),\n        os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)\n    with os.fdopen(fd, &quot;w&quot;) as f:\n        json.dump(item, f, ensure_ascii=False)\n    return {&quot;request_id&quot;: rid, &quot;action&quot;: action,\n        &quot;table&quot;: table, &quot;row_id&quot;: row_id, &quot;before&quot;: before,\n        &quot;values&quot;: values, &quot;state&quot;: &quot;awaiting SSH approval&quot;}\n\n@mcp.tool()\ndef apply_change(request_id: str) -&gt; dict:\n    &quot;&quot;&quot;Execute one approved change, with audit and retry protection.&quot;&quot;&quot;\n    with request_path(request_id).open(&quot;r+&quot;) as f:\n        fcntl.flock(f, fcntl.LOCK_EX)\n        req = json.load(f)\n        if req[&quot;status&quot;] == &quot;applied&quot;:\n            return {&quot;status&quot;: &quot;already_applied&quot;}\n        if not req[&quot;approved&quot;] or req[&quot;status&quot;] != &quot;pending&quot;:\n            raise PermissionError(&quot;Local approval required&quot;)\n        if datetime.now(timezone.utc) &gt;= datetime.fromisoformat(req[&quot;expires&quot;]):\n            raise ValueError(&quot;Request expired&quot;)\n        action, table, row_id = req[&quot;action&quot;], req[&quot;table&quot;], req[&quot;row_id&quot;]\n        p = policy(table)\n        if action == &quot;delete&quot; and not p.get(&quot;delete&quot;, False):\n            raise ValueError(&quot;DELETE disabled&quot;)\n        values = valid_values(table, action, req[&quot;values&quot;])\n        conn = db()\n        try:\n            conn.start_transaction()\n            cur = conn.cursor(dictionary=True, buffered=True)\n            try:\n                cur.execute(&quot;SELECT request_id FROM mcp_change_log WHERE request_id=%s&quot;,\n                    (request_id,))\n                if cur.fetchone():\n                    conn.rollback()\n                    return {&quot;status&quot;: &quot;already_applied&quot;}\n                if action != &quot;insert&quot;:\n                    current = one(conn, table, row_id, lock=True)\n                    if current != req[&quot;before&quot;]:\n                        raise ValueError(&quot;Row changed since preview&quot;)\n                if action == &quot;insert&quot;:\n                    cols = list(values)\n                    sql = (f&quot;INSERT INTO {quoted(table)} (&quot; +\n                        &quot;,&quot;.join(map(quoted, cols)) + &quot;) VALUES (&quot; +\n                        &quot;,&quot;.join([&quot;%s&quot;] * len(cols)) + &quot;)&quot;)\n                    cur.execute(sql, tuple(values[c] for c in cols))\n                    row_id = cur.lastrowid\n                elif action == &quot;update&quot;:\n                    cols = list(values)\n                    sql = (f&quot;UPDATE {quoted(table)} SET &quot; +\n                        &quot;,&quot;.join(f&quot;{quoted(c)}=%s&quot; for c in cols) +\n                        f&quot; WHERE {quoted(p['pk'])}=%s&quot;)\n                    cur.execute(sql, (*[values[c] for c in cols], row_id))\n                elif action == &quot;delete&quot;:\n                    cur.execute(\n                        f&quot;DELETE FROM {quoted(table)} WHERE {quoted(p['pk'])}=%s&quot;,\n                        (row_id,))\n                if cur.rowcount != 1:\n                    raise ValueError(&quot;Exactly one changed row required&quot;)\n                cur.execute(\n                    &quot;INSERT INTO mcp_change_log(request_id,action,table_name,target_id) &quot;\n                    &quot;VALUES(%s,%s,%s,%s)&quot;,\n                    (request_id, action, table, row_id))\n                conn.commit()\n                req[&quot;status&quot;] = &quot;applied&quot;\n                f.seek(0); json.dump(req, f, ensure_ascii=False)\n                f.truncate(); f.flush(); os.fsync(f.fileno())\n                return {&quot;status&quot;: &quot;applied&quot;, &quot;request_id&quot;: request_id,\n                        &quot;action&quot;: action, &quot;row_id&quot;: row_id}\n            finally:\n                cur.close()\n        except Exception:\n            conn.rollback()\n            raise\n        finally:\n            conn.close()\n\nif __name__ == &quot;__main__&quot;:\n    mcp.run(transport=&quot;streamable-http&quot;, host=&quot;127.0.0.1&quot;,\n            port=8000, stateless_http=True, json_response=True)<\/code><\/pre>\n<h2>6. Ubuntu \uad00\ub9ac\uc790 \uc2b9\uc778 \ucf54\ub4dc: approve.py<\/h2>\n<p>\uc774 \ud30c\uc77c\uc740 MCP \ub3c4\uad6c\ub85c \uc81c\uacf5\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uad00\ub9ac\uc790\uac00 SSH\uc5d0\uc11c \uc694\uccad ID\uc640 \ubcc0\uacbd \uc804 \uac12\uc744 \uc9c1\uc811 \ud655\uc778\ud574\uc57c \ud569\ub2c8\ub2e4.<\/p>\n<pre class=\"wp-block-code\"><code class=\"language-python\">import fcntl, json, os, re, sys\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\nrid = sys.argv[1] if len(sys.argv) == 2 else &quot;&quot;\nif not re.fullmatch(r&quot;[0-9a-f]{32}&quot;, rid):\n    sys.exit(&quot;Usage: approve.py 32-character-request-id&quot;)\npath = Path(&quot;\/var\/lib\/mysql-mcp\/pending&quot;) \/ (rid + &quot;.json&quot;)\nwith path.open(&quot;r+&quot;) as f:\n    fcntl.flock(f, fcntl.LOCK_EX)\n    item = json.load(f)\n    if item[&quot;status&quot;] != &quot;pending&quot;:\n        sys.exit(&quot;Not pending&quot;)\n    if datetime.now(timezone.utc) &gt;= datetime.fromisoformat(item[&quot;expires&quot;]):\n        sys.exit(&quot;Expired&quot;)\n    print(json.dumps(item, ensure_ascii=False, indent=2))\n    if input(&quot;Type APPROVE: &quot;).strip() != &quot;APPROVE&quot;:\n        sys.exit(&quot;Cancelled&quot;)\n    item[&quot;approved&quot;] = True\n    f.seek(0)\n    json.dump(item, f, ensure_ascii=False)\n    f.truncate()\n    f.flush()\n    os.fsync(f.fileno())\nprint(&quot;Approved&quot;)<\/code><\/pre>\n<h2>7. \ud658\uacbd \ud30c\uc77c\u00b7\uad8c\ud55c\u00b7systemd<\/h2>\n<pre class=\"wp-block-code\"><code class=\"language-bash\">sudo useradd --system --home-dir \/var\/lib\/mysql-mcp --shell \/usr\/sbin\/nologin mcpreader\nsudo install -d -m 700 -o mcpreader -g mcpreader \/var\/lib\/mysql-mcp\/pending\nsudo install -d -m 700 \/etc\/mysql-mcp\nsudo nano \/etc\/mysql-mcp\/mcp.env\n# \uc544\ub798 \ub0b4\uc6a9\uc744 \uc785\ub825 (\uc2e4\uc81c \ube44\ubc00\ubc88\ud638\ub85c \ubcc0\uacbd):\n# MYSQL_USER=mcp_app\n# MYSQL_PASSWORD=CHANGE_TO_STRONG_PASSWORD\n# MYSQL_DATABASE=sampledb\nsudo chown root:root \/etc\/mysql-mcp\/mcp.env\nsudo chmod 600 \/etc\/mysql-mcp\/mcp.env\nsudo chown -R root:root \/opt\/mysql-mcp\nsudo chmod 755 \/opt\/mysql-mcp\nsudo nano \/etc\/systemd\/system\/mysql-mcp.service<\/code><\/pre>\n<pre class=\"wp-block-code\"><code class=\"language-ini\">[Unit]\nDescription=Python MySQL MCP\nAfter=network.target\n[Service]\nType=simple\nUser=mcpreader\nGroup=mcpreader\nWorkingDirectory=\/opt\/mysql-mcp\nEnvironmentFile=\/etc\/mysql-mcp\/mcp.env\nExecStart=\/opt\/mysql-mcp\/.venv\/bin\/python \/opt\/mysql-mcp\/server.py\nRestart=on-failure\nRestartSec=5\nNoNewPrivileges=true\nPrivateTmp=true\nProtectHome=true\nProtectSystem=strict\nReadWritePaths=\/var\/lib\/mysql-mcp\/pending\n[Install]\nWantedBy=multi-user.target<\/code><\/pre>\n<pre class=\"wp-block-code\"><code class=\"language-bash\">sudo \/opt\/mysql-mcp\/.venv\/bin\/python -m py_compile \/opt\/mysql-mcp\/server.py \/opt\/mysql-mcp\/approve.py\nsudo systemctl daemon-reload\nsudo systemctl enable --now mysql-mcp\nsudo systemctl status mysql-mcp\nsudo journalctl -u mysql-mcp -n 50 --no-pager<\/code><\/pre>\n<h2>8. \ub85c\uceec \uc870\ud68c \ubc0f UPDATE \ud14c\uc2a4\ud2b8<\/h2>\n<p>SDK\uc758 \uc138\uc158\/\ud504\ub85c\ud1a0\ucf5c \ubc84\uc804\uc5d0 \ub530\ub77c \ud5e4\ub354\uac00 \ub2ec\ub77c\uc9c8 \uc218 \uc788\uc73c\ubbc0\ub85c MCP Inspector \uc0ac\uc6a9\ub3c4 \uad8c\uc7a5\ud569\ub2c8\ub2e4. \ub85c\uceec CLI \ud14c\uc2a4\ud2b8 \uc608\uc2dc\ub294 \ub2e4\uc74c\uacfc \uac19\uc2b5\ub2c8\ub2e4.<\/p>\n<pre class=\"wp-block-code\"><code class=\"language-bash\">curl -sS http:\/\/127.0.0.1:8000\/mcp \\\n -H 'Content-Type: application\/json' \\\n -H 'Accept: application\/json, text\/event-stream' \\\n -H 'MCP-Protocol-Version: 2026-07-28' \\\n -d '{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;method&quot;:&quot;tools\/call&quot;,&quot;params&quot;:{&quot;name&quot;:&quot;list_rows&quot;,&quot;arguments&quot;:{&quot;table&quot;:&quot;mcp_demo_items&quot;,&quot;limit&quot;:5}}}'<\/code><\/pre>\n<p>UPDATE \uc608: <code>prepare_change<\/code>\uc5d0 <code>{\"action\":\"update\",\"table\":\"mcp_demo_items\",\"row_id\":1,\"values\":{\"price\":32000}}<\/code> \uc804\ub2ec \u2192 \ubc18\ud658\ub41c request_id\ub97c \uae30\ub85d \u2192 SSH\uc5d0\uc11c \uc2b9\uc778 \u2192 <code>apply_change(request_id)<\/code> \ud638\ucd9c \u2192 SELECT\ub85c \uacb0\uacfc \ud655\uc778.<\/p>\n<pre class=\"wp-block-code\"><code class=\"language-bash\">sudo python3 \/opt\/mysql-mcp\/approve.py \uc2e4\uc81c_32\uc790\ub9ac_request_id\n# APPROVE \uc785\ub825 \ud6c4 ChatGPT\/MCP \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0\uc11c apply_change \ud638\ucd9c<\/code><\/pre>\n<h2>9. Secure MCP Tunnel \u00b7 ChatGPT \ub4f1\ub85d<\/h2>\n<p>OpenAI Platform\uc5d0\uc11c tunnel_id\uc640 Runtime API Key \ubc1c\uae09 \u2192 tunnel-client \ucd5c\uc2e0 \uc2e4\ud589 \ud30c\uc77c \uc124\uce58 \u2192 \ub2e4\uc74c\uacfc \uac19\uc774 HTTP MCP \ud504\ub85c\ud544\uc744 \ucd08\uae30\ud654\ud569\ub2c8\ub2e4. tunnel-client CLI \uc778\uc790\ub294 \uacf5\uc2dd \ubb38\uc11c\ub97c \ucd5c\uc2e0 \uae30\uc900\uc73c\ub85c \ud655\uc778\ud558\uc138\uc694.<\/p>\n<pre class=\"wp-block-code\"><code class=\"language-bash\">export CONTROL_PLANE_API_KEY=&quot;YOUR_RUNTIME_API_KEY&quot;\ntunnel-client help quickstart\ntunnel-client init --profile local-mysql --tunnel-id YOUR_TUNNEL_ID \\\n  --mcp-server-url http:\/\/127.0.0.1:8000\/mcp\ntunnel-client doctor --profile local-mysql --explain\ntunnel-client run --profile local-mysql<\/code><\/pre>\n<p>ChatGPT \u2192 Plugins \u2192 + \u2192 Add custom MCP server \u2192 Connection: Tunnel\uc5d0\uc11c \ud130\ub110\uc744 \uc120\ud0dd\ud558\uace0 \ub3c4\uad6c\ub97c \ud655\uc778\ud569\ub2c8\ub2e4. \uc870\uc9c1\ubcc4 \uc5f0\uacb0 \uad8c\ud55c\uc774 \ud544\uc694\ud558\uba70, 2026\ub144 10\uc6d4 \uae30\uc900 \uc0ac\uc6a9\uc790 \uc9c0\uc815 MCP \uc4f0\uae30\/\uc218\uc815 \uae30\ub2a5\uc740 Business\u00b7Enterprise\u00b7Edu\uc5d0\uc11c \ubca0\ud0c0 \uc81c\uacf5\ub429\ub2c8\ub2e4. \uac1c\uc778 Plus\uc5d0\uc11c\ub294 \ub3d9\uc77c\ud55c \uc4f0\uae30 \uc5f0\uacb0 \uae30\ub2a5\uc774 \uc81c\uacf5\ub41c\ub2e4\uace0 \ubcf4\uc7a5\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.<\/p>\n<h2>10. \uc6b4\uc601 \uc2dc \uc8fc\uc758\uc0ac\ud56d<\/h2>\n<p><strong>\uc774 \ucf54\ub4dc\ub294 \ud559\uc2b5\u00b7\ud14c\uc2a4\ud2b8\uc6a9 \uad6c\ud604 \uc608\uc81c\uc774\uba70 \uc2e4\uc81c \uc11c\ubc84\uc5d0\uc11c \ube4c\ub4dc\u00b7\ud1b5\ud569 \uac80\uc99d\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.<\/strong> \uc6b4\uc601 \ubc18\uc601 \uc804 DB \ubc31\uc5c5, \uac1c\uc778\uc815\ubcf4 \uceec\ub7fc \uc81c\uac70, OAuth\/\uc6cc\ud06c\uc2a4\ud398\uc774\uc2a4 \uad8c\ud55c \uc124\uc815, \ucffc\ub9ac \ud0c0\uc784\uc544\uc6c3\u00b7\uc694\uccad \uc81c\ud55c, \ubcc0\uacbd \ub85c\uadf8 \ubaa8\ub2c8\ud130\ub9c1, \ud2b8\ub9ac\uac70\u00b7\uc678\ub798\ud0a4 \uc5f0\uc1c4 \uc791\uc5c5 \ud655\uc778\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. DB \uc0ac\uc6a9\uc790\uc5d0\uac8c CREATE\/ALTER\/DROP\/GRANT \uad8c\ud55c\uc744 \uc8fc\uc9c0 \ub9c8\uc138\uc694.<\/p>\n<h2>\uacf5\uc2dd \ucc38\uace0 \ubb38\uc11c<\/h2>\n<ul>\n<li><a href=\"https:\/\/py.sdk.modelcontextprotocol.io\/run\/\">Python MCP SDK \uc2e4\ud589<\/a><\/li>\n<li><a href=\"https:\/\/developers.openai.com\/api\/docs\/guides\/secure-mcp-tunnels\">OpenAI Secure MCP Tunnel<\/a><\/li>\n<li><a href=\"https:\/\/help.openai.com\/en\/articles\/12584461-connecting-custom-mcp-servers-in-chatgpt\">ChatGPT \uc0ac\uc6a9\uc790 \uc815\uc758 MCP \uc5f0\uacb0<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Ubuntu 24.04\uc5d0\uc11c Python MCP \uc11c\ubc84\ub97c \ub9cc\ub4e4\uace0 MySQL\uc758 SELECT\u00b7INSERT\u00b7UPDATE\u00b7DELETE\ub97c SSH \uc2b9\uc778, \ucd5c\uc18c \uad8c\ud55c, Secure MCP Tunnel\ub85c \uc81c\uc5b4\ud558\ub294 \ub2e8\uacc4\ubcc4 \uac00\uc774\ub4dc.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1026","post","type-post","status-publish","format-standard","hentry","category-1"],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/csharp.ihavenomoney.co.kr\/index.php?rest_route=\/wp\/v2\/posts\/1026","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/csharp.ihavenomoney.co.kr\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/csharp.ihavenomoney.co.kr\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/csharp.ihavenomoney.co.kr\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/csharp.ihavenomoney.co.kr\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1026"}],"version-history":[{"count":0,"href":"https:\/\/csharp.ihavenomoney.co.kr\/index.php?rest_route=\/wp\/v2\/posts\/1026\/revisions"}],"wp:attachment":[{"href":"https:\/\/csharp.ihavenomoney.co.kr\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1026"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/csharp.ihavenomoney.co.kr\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1026"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/csharp.ihavenomoney.co.kr\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1026"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}