1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VSCodeでMCPを使って、ノーコードでWebアプリ開発

Last updated at Posted at 2025-04-12

はじめに

ついにVSCodeでも簡単にMCPが使えるようになったので、試しにノーコード開発でtodoアプリを作ってみました。

必要なもの

  • VSCode
  • Githubアカウント
  • Docker(オプション)

開発手順

  • node環境のdevcontainerをセットアップする
devcontainer.json
{
	"name": "Node.js & TypeScript",
	"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm"
}

devcontainerを使用しない場合は、ローカルにnodeをインストールしてください。

  • VSCodeでGithub Copilotをセットアップする

無料でも利用できますが、Githubアカウントが必要になります。

  • .vscode/mcp.jsonファイルにfilesystemMCPサーバーの設定を書く
mcp.json
{
    "servers": {
        "filesystem": {
            "type": "stdio",
            "command": "npx",
            "args": [
                "-y",
                "@modelcontextprotocol/server-filesystem",
                "${workspaceFolder}",
              ]
        }
    }
}

これにより、Github CopilotのAgentがワークスペースでファイルを読み書きできるようになります。

  • Github Copilotのチャットを開き、Agentモードでtodoアプリの開発を依頼する
todoアプリをhtml,css,jsで作ってください。

生成されたファイルが以下になります。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todoアプリ</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Todoリスト</h1>
        
        <div class="todo-input">
            <input type="text" id="todo-input" placeholder="タスクを入力...">
            <button id="add-button">追加</button>
        </div>
        
        <div class="filter-buttons">
            <button id="filter-all" class="active">すべて</button>
            <button id="filter-active">未完了</button>
            <button id="filter-completed">完了済み</button>
        </div>
        
        <ul id="todo-list"></ul>
        
        <div class="todo-footer">
            <span id="items-left">0 項目が残っています</span>
            <button id="clear-completed">完了済みを削除</button>
        </div>
    </div>
    
    <script src="script.js"></script>
</body>
</html>
script.js
document.addEventListener('DOMContentLoaded', () => {
    // DOM elements
    const todoInput = document.getElementById('todo-input');
    const addButton = document.getElementById('add-button');
    const todoList = document.getElementById('todo-list');
    const filterAll = document.getElementById('filter-all');
    const filterActive = document.getElementById('filter-active');
    const filterCompleted = document.getElementById('filter-completed');
    const itemsLeft = document.getElementById('items-left');
    const clearCompleted = document.getElementById('clear-completed');
    
    // Load todos from localStorage
    let todos = JSON.parse(localStorage.getItem('todos')) || [];
    let filter = 'all';
    
    // Render todos
    const renderTodos = () => {
        todoList.innerHTML = '';
        
        const filteredTodos = todos.filter(todo => {
            if (filter === 'active') return !todo.completed;
            if (filter === 'completed') return todo.completed;
            return true;
        });
        
        filteredTodos.forEach((todo, index) => {
            const li = document.createElement('li');
            
            const todoItem = document.createElement('div');
            todoItem.classList.add('todo-item');
            if (todo.completed) {
                todoItem.classList.add('completed');
            }
            
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.checked = todo.completed;
            checkbox.addEventListener('change', () => toggleComplete(todo.id));
            
            const todoText = document.createElement('span');
            todoText.textContent = todo.text;
            
            todoItem.appendChild(checkbox);
            todoItem.appendChild(todoText);
            
            const deleteBtn = document.createElement('button');
            deleteBtn.classList.add('delete-btn');
            deleteBtn.innerHTML = '×';
            deleteBtn.addEventListener('click', () => deleteTodo(todo.id));
            
            li.appendChild(todoItem);
            li.appendChild(deleteBtn);
            todoList.appendChild(li);
        });
        
        updateItemsLeft();
    };
    
    // Add a new todo
    const addTodo = () => {
        const text = todoInput.value.trim();
        if (text !== '') {
            const todo = {
                id: Date.now(),
                text,
                completed: false
            };
            
            todos.push(todo);
            saveTodos();
            todoInput.value = '';
            renderTodos();
        }
    };
    
    // Toggle todo completion status
    const toggleComplete = (id) => {
        todos = todos.map(todo => {
            if (todo.id === id) {
                return { ...todo, completed: !todo.completed };
            }
            return todo;
        });
        
        saveTodos();
        renderTodos();
    };
    
    // Delete a todo
    const deleteTodo = (id) => {
        todos = todos.filter(todo => todo.id !== id);
        saveTodos();
        renderTodos();
    };
    
    // Clear all completed todos
    const clearCompletedTodos = () => {
        todos = todos.filter(todo => !todo.completed);
        saveTodos();
        renderTodos();
    };
    
    // Save todos to localStorage
    const saveTodos = () => {
        localStorage.setItem('todos', JSON.stringify(todos));
    };
    
    // Update items left counter
    const updateItemsLeft = () => {
        const activeCount = todos.filter(todo => !todo.completed).length;
        itemsLeft.textContent = `${activeCount} 項目が残っています`;
    };
    
    // Set active filter
    const setFilter = (selectedFilter) => {
        filter = selectedFilter;
        
        // Update active class for filter buttons
        filterAll.classList.remove('active');
        filterActive.classList.remove('active');
        filterCompleted.classList.remove('active');
        
        if (selectedFilter === 'all') {
            filterAll.classList.add('active');
        } else if (selectedFilter === 'active') {
            filterActive.classList.add('active');
        } else if (selectedFilter === 'completed') {
            filterCompleted.classList.add('active');
        }
        
        renderTodos();
    };
    
    // Event listeners
    addButton.addEventListener('click', addTodo);
    todoInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
            addTodo();
        }
    });
    
    filterAll.addEventListener('click', () => setFilter('all'));
    filterActive.addEventListener('click', () => setFilter('active'));
    filterCompleted.addEventListener('click', () => setFilter('completed'));
    clearCompleted.addEventListener('click', clearCompletedTodos);
    
    // Initial render
    renderTodos();
});
style.css
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Helvetica', 'Arial', sans-serif;
}

body {
    background-color: #f5f5f5;
    color: #333;
    line-height: 1.6;
    padding: 20px;
}

.container {
    max-width: 600px;
    margin: 0 auto;
    background-color: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

h1 {
    text-align: center;
    margin-bottom: 20px;
    color: #2c3e50;
}

.todo-input {
    display: flex;
    margin-bottom: 20px;
}

#todo-input {
    flex: 1;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px 0 0 4px;
    font-size: 16px;
}

#add-button {
    padding: 10px 15px;
    background-color: #3498db;
    color: #fff;
    border: none;
    border-radius: 0 4px 4px 0;
    cursor: pointer;
    font-size: 16px;
    transition: background-color 0.3s;
}

#add-button:hover {
    background-color: #2980b9;
}

.filter-buttons {
    display: flex;
    justify-content: center;
    margin-bottom: 15px;
}

.filter-buttons button {
    margin: 0 5px;
    padding: 5px 10px;
    background-color: transparent;
    border: 1px solid #ddd;
    border-radius: 4px;
    cursor: pointer;
    transition: all 0.3s;
}

.filter-buttons button.active {
    background-color: #3498db;
    color: white;
    border-color: #3498db;
}

#todo-list {
    list-style-type: none;
}

#todo-list li {
    padding: 10px 15px;
    border-bottom: 1px solid #eee;
    display: flex;
    align-items: center;
    transition: background-color 0.2s;
}

#todo-list li:hover {
    background-color: #f9f9f9;
}

.todo-item input[type="checkbox"] {
    margin-right: 10px;
    cursor: pointer;
}

.todo-item.completed span {
    color: #999;
    text-decoration: line-through;
}

.todo-item {
    display: flex;
    align-items: center;
    flex: 1;
}

.delete-btn {
    color: #e74c3c;
    background: none;
    border: none;
    font-size: 16px;
    cursor: pointer;
    opacity: 0.7;
    transition: opacity 0.3s;
}

.delete-btn:hover {
    opacity: 1;
}

.todo-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-top: 15px;
    margin-top: 15px;
    border-top: 1px solid #eee;
    font-size: 14px;
}

#items-left {
    color: #7f8c8d;
}

#clear-completed {
    background: none;
    border: none;
    color: #7f8c8d;
    cursor: pointer;
    text-decoration: underline;
}

#clear-completed:hover {
    color: #e74c3c;
}
  • ブラウザで開いて動作確認をする

image.png

指示していない細かな部分もつくってくれていました。

おわりに

今回は課金してClaude 3.7 Sonnetを使いましたが、無料版で使えるgpt-4oでもtodoアプリ程度であれば正しく実装できていました。
filesystem以外のMCPサーバーも追加していけば、VSCode上でもDevinやClineのようなAI駆動開発ができてしまいそうな可能性を感じました。

MCPサーバーの一覧はこちらから確認できます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?