1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

私PowershellだけどMCPサーバーになりたい

Last updated at Posted at 2025-10-26

3行で要約

  • お手軽MCPサーバー体験
  • Windows+PowerShellで動作する追加パッケージ不要のMCPサーバーの実現方法を紹介
  • 独自のMCPメソッドを追加可能なps1サンプル(300行)

はじめに

MCP便利ですよね。
私はCursor使って開発していますが、chrome-devtools-mcpが出てから使わない日は無いです。
ですが、まだ痒いところに手が届くようなMCPを探すのが難しく、カスタムしたり自作するスキルを習得したいと考えていました。

ちょうどよく上記の本が出たので購入して読んだのですが、Python + FastMCP という構成でMCPサーバーを作るという手順で記載されていました。
コマンドやシーケンスの説明などは参考になりましたが、FastMCPを使うと下回りを理解せずにサーバー開発している感じがして嫌だったので、練習として全部powershellで自作してみることにしました。

環境

私が使っている環境は以下です。

  • Windows 11
  • Powershell 5.1 (OS標準で動く環境を想定)
  • Cursor
  • GeminiCLI

MCPって何なの?どういう技術?

MCPとは

Model Context Protocol の略で、LLMと連携するツール・データソースのプロトコロルになります。

これまではLLMの応答を元に人間が手動でツールを操作して、ツールから得られた結果を再度LLMに手動で送るということをしていました。
MCPを使うことLLM側でツールを自動実行し、結果もそのままLLM側で取得可能になるためかなりの無駄が省けます。

通信方式

以下の2パターンがあります。

  • stdio(標準入出力)
  • Streamable HTTP

今回はローカルで最低限の実装をするのでstdioで作りました。

データ形式

JSON-RPC 2.0 という形式だそうです。
あまり理解していませんが、通信相手の応答を見ながら実装できたのでそんなに難しくはありません。
JSONのパースは自前でしたくないのでパーサーは必要だと思います。

Powershell-MCPサーバーを使ってみる

サンプルを動作させてみよう

数値をインクリメント、デクリメントするサンプルのスクリプトを動かしてみましょう。

  1. スクリプトの用意

githubにソース一式あるのでダウンロードするか以下からコピペしてください。

コピペする場合(UTF8-With-BOMで保存)
mcp_server.ps1

# PowerShell MCP Server

# Console入出力の設定
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8

# ログ出力関数
$script:appPath = Split-Path $MyInvocation.MyCommand.Path -Parent
$script:appLogFile = Join-Path $script:appPath "powershell_mcp_server.log"
function Write-Log {
    param([string]$Message)
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $line = "[$timestamp] $Message"
    Add-Content -Path $script:appLogFile -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue
}

# MCPコマンドインターフェース
class MCPCommandInterface {
    [string]$MethodName = $null
    [string]$Description = $null
    [hashtable]$InputSchema = @{}

    [object] Invoke([object]$Params) {
        throw "Not implemented"
    }

    [void] Validate([object]$Params) {
        throw "Not implemented"
    }
}

# インクリメントコマンド
class MCPCommandIncrement : MCPCommandInterface {
    [string]$MethodName = "increment"
    [string]$Description = "数値をインクリメント(+1)します"
    [hashtable]$InputSchema = @{type = "object"; properties = @{number = @{type = "number"; description = "インクリメントする数値"}}; required = @("number")}

    [object] Invoke([object]$Params) {
        $number = $Params.number
        $result = $number + 1
        Write-Log "数値 $number をインクリメント: $result"
        
        $content = @()
        $contentItem = @{
            type = "text"
            text = $result.ToString()
        }
        $content += $contentItem
        
        return @{
            content = $content
        }
    }

    [void] Validate([object]$Params) {
        if ($null -eq $Params.number) {
            throw "パラメータ 'number' が指定されていません"
        }
        
        $number = $Params.number
        if ($number -isnot [int] -and $number -isnot [double] -and $number -isnot [decimal]) {
            throw "パラメータ 'number' は数値である必要があります"
        }
    }
}

# デクリメントコマンド
class MCPCommandDecrement : MCPCommandInterface {
    [string]$MethodName = "decrement"
    [string]$Description = "数値をデクリメント(-1)します"
    [hashtable]$InputSchema = @{type = "object"; properties = @{number = @{type = "number"; description = "デクリメントする数値"}}; required = @("number")}
    
    [object] Invoke([object]$Params) {
        $number = $Params.number
        $result = $number - 1
        Write-Log "数値 $number をデクリメント: $result"
        
        $content = @()
        $contentItem = @{
            type = "text"
            text = $result.ToString()
        }
        $content += $contentItem
        
        return @{
            content = $content
        }
    }

    [void] Validate([object]$Params) {
        if ($null -eq $Params.number) {
            throw "パラメータ 'number' が指定されていません"
        }
        
        $number = $Params.number
        if ($number -isnot [int] -and $number -isnot [double] -and $number -isnot [decimal]) {
            throw "パラメータ 'number' は数値である必要があります"
        }
    }
}

# MCPサーバークラス
class MCPServer {
    [hashtable]$Commands = @{}
    static [string]$ProtocolVersion = "2025-06-18"
 
    # JSON-RPC受信
    [object] ReadNextRequest() {
        try {
            $line = [Console]::In.ReadLine()
            if ($null -eq $line) { 
                return $null 
            }
            $trimmed = $line.Trim()
            if ([string]::IsNullOrEmpty($trimmed)) {
                return $null
            }
            return $trimmed
        }
        catch {
            Write-Log "読み取りエラー: $($_.Exception.Message)"
            return $null
        }
    }

    # JSON-RPC送信
    [void] WriteResponse([string]$Json) {
        try {
            $stdout = [Console]::OpenStandardOutput()
            $line = $Json + "`n"
            $bytes = [System.Text.Encoding]::UTF8.GetBytes($line)
            $stdout.Write($bytes, 0, $bytes.Length)
            $stdout.Flush()
        } catch {
            Write-Log "WriteResponse エラー: $($_.Exception.Message)"
            throw
        }
    }

    # JSON-RPC 成功レスポンス作成
    [hashtable] SuccessResponse([object]$Id, [object]$Result) {
        return @{
            jsonrpc = "2.0"
            id = $Id
            result = $Result
        }
    }

    # コマンド追加
    [void] addCommand([MCPCommandInterface]$command) {
        $this.Commands.Add($command.MethodName, $command)
    }

    # サーバー実行
    [boolean] run() {
            Write-Log "PowerShell MCP Server を開始します"
            
            try {
                while ($true) {
                    $inputBody = $this.ReadNextRequest()
                    if ($null -eq $inputBody) { 
                        Write-Log "EOF検出: サーバーを終了します"
                        break 
                    }

                    Write-Log "受信: $inputBody"
        
                    $request = $null
                    $response = $null
                    $requestId = $null
        
                    # JSONパース
                    try {
                        $request = $inputBody | ConvertFrom-Json
                        $requestId = $request.id
                    }
                    catch {
                        Write-Log "JSON解析エラー: $($_.Exception.Message)"
                        exit 1
                    }
        
                    try {
                        # 通知メソッドの場合は応答不要
                        if ($null -eq $requestId) {
                            Write-Log "通知メソッド: $($request.method)"
                            continue
                        }
                        
                        # ドットで区切るクライアントがあるらしいのでスラッシュにする
                        $normalizedMethod = $request.method -replace '\.', '/'
                        
                        switch ($normalizedMethod) {
                            "initialize" {
                                $capabilities = @{}
                                $capabilities.tools = @{}
        
                                $result = @{
                                    protocolVersion = [MCPServer]::ProtocolVersion
                                    capabilities = $capabilities
                                    serverInfo = @{
                                        name = "PowerShell MCP Server"
                                        version = "1.0.0"
                                    }
                                }
                                $response = $this.SuccessResponse($requestId, $result)
                            }
        
                            "ping" {
                                # Gemini クライアント用の ping 対応
                                $result = @{}
                                $response = $this.SuccessResponse($requestId, $result)
                            }
        
                            "tools/list" {
                                $tools = @()
                                foreach ($command in $this.Commands.Values) {
                                    $tool = @{
                                        name = $command.MethodName
                                        description = $command.Description
                                        inputSchema = $command.InputSchema
                                    }
                                    $tools += $tool
                                }       
                                $result = @{
                                    tools = $tools
                                }
                                $response = $this.SuccessResponse($requestId, $result)
                            }
        
                            "tools/call" {

                                foreach ($command in $this.Commands.Values) {
                                    if ($request.params.name -eq $command.MethodName) {
                                        try {
                                            $command.Validate($request.params.arguments)
                                            $result = $command.Invoke($request.params.arguments)
                                            $response = $this.SuccessResponse($requestId, $result)
                                        }
                                        catch {
                                            Write-Log "パラメータ検証エラー: $($_.Exception.Message)"
                                            exit 1
                                        }
                                        break
                                    }
                                }
                                if ($null -eq $response) {
                                    Write-Log "メソッドが見つかりません: $($request.params.name)"
                                    exit 1
                                }
                            }
                            default {
                                Write-Log "未知のメソッド: $($request.method)"
                                exit 1
                            }
                        }
                    }
                    catch {
                        Write-Log "メソッド処理エラー: $($_.Exception.Message)"
                        exit 1
                    }
        
                    try {
                        $jsonResponse = $response | ConvertTo-Json -Compress -Depth 10
                        $this.WriteResponse($jsonResponse)
                        Write-Log "送信: $jsonResponse"
                    } catch {
                        Write-Log "レスポンス処理エラー: $($_.Exception.Message)"
                        exit 1
                    }
                }
            }
            catch {
                Write-Log "サーバーエラー: $($_.Exception.Message)"
                exit 1
            }
      return $null
    }
}
  
# サーバー開始
$mcpServer = [MCPServer]::new()
$mcpServer.addCommand([MCPCommandIncrement]::new())
$mcpServer.addCommand([MCPCommandDecrement]::new())
$mcpServer.run()


C:\mcp_server\mcp_server.ps1
に配置する想定で以降記載します。

  1. 単体スクリプトの動作確認

Windowsターミナルで以下のコマンドで動作するか確認してみましょう。


echo '{"jsonrpc":"2.0","id":1,"method":"ping","params":{}}' | powershell -File "mcp_server.ps1"

を実行して

{"result":{},"id":1,"jsonrpc":"2.0"}

が返ってくればOKです。

C:\mcp_server\powershell_mcp_server.log というログファイルを出力するのでエラーになっていないか確認してください。


  1. MCPサーバーの登録と動作確認

geminiCLIとCursorで動作確認したのでそれぞれの設定方法を記載します。

  • geminiCLI

MCPを使いたいプロジェクトのフォルダに
.gemini\settings.json
を用意する

settings.json
{
  "mcpServers": {
    "powershell-mcp": {
      "command": "powershell.exe",
      "args": [
        "-NoProfile",
        "-ExecutionPolicy", 
        "Bypass",
        "-File", 
        "C:\\mcp_server\\mcp_server.ps1"
      ]
    }
  }
}

プロジェクトのフォルダで以下を実行し、Connected の表示がされることを確認

>gemini mcp list
Configured MCP servers:

✓ powershell-increment: powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\\mcp_server\\mcp_server.ps1 (stdio) - Connected

geminiCLIを起動し、プロンプトに以下のような文言でMCPをテストする

powershell-mcp というMCPサーバーを作成したので呼び出しテストをしてください。インクリメントメソッドを 4 で実行し、結果を教えてください。

MCP実行前に確認されるので許可する

gemini_allow.png

4 + 1 = 5 であることを得られる。

gemini_result.png


  • cursor

C:\Users\{ユーザー名}\.cursor\mcp.json
に、mcpサーバーを登録します。

mcp.json
{
  "mcpServers": {
      "powershell-mcp": {
        "command": "powershell.exe",
        "args": [
          "-NoProfile",
          "-ExecutionPolicy",
          "Bypass",
          "-File", 
          "C:\\mcp_server\\mcp_server.ps1"
        ]
      }
  }
}

cursorを起動して、設定を確認します。

[ファイル] - [ユーザー設定] - [Cursor Settings] で、
[Tools & MCP]をクリック

緑アイコンと 2 tools enabled の表示になっていればOK

cursot_tools1.png

2 tools enabledをクリックすると Increment と Decrement が認識していることを確認できます。

cursot_tools2.png

プロンプトに以下のような文言でMCPをテストする

powershell-mcp というMCPサーバーを作成したので呼び出しテストをしてください。インクリメントメソッドを 4 で実行し、結果を教えてください。

MCP実行前に確認されるので許可(Run)する

cursor_run.png

4 + 1 = 5 であることを得られる。

cursor_run_result.png

サンプルにコマンドを追加してみよう

コマンドのクラスを MCPCommandInterface でインターフェース化してあるので追加も簡単です。
Windowsのトーストで通知するメソッド MCPCommandNotify を作る例です。
※トーストの表示についてはこちらを参照

  • コマンドクラスを追加

# MCPCommandInterface を継承してクラスを宣言

class MCPCommandNotify : MCPCommandInterface {

    # メソッド名
    [string]$MethodName = "notify"
    # メソッドの説明
    [string]$Description = "通知メソッド"
    # パラメータの定義
    [hashtable]$InputSchema = @{type = "object"; properties = @{message = @{type = "string"; description = "通知メッセージ"}; detail = @{type = "string"; description = "通知詳細"}}}
    
    # callされるメソッド
    [object] Invoke([object]$Params) {
        
        [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
        [Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
        [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
        $app_id = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'

        Write-Log "通知メッセージ: message=$($Params.message), detail=$($Params.detail)"

        $notificationTitle = "PowerShell MCP Server"
        $notificationMessage = $Params.message
        $notificationDetail = $Params.detail
        
        $xmlContent = @"
<?xml version="1.0" encoding="utf-8"?>
<toast>
  <visual>
    <binding template="ToastGeneric">
      <text>$($notificationTitle)</text>
      <text>$($notificationMessage)</text>
      <text>$($notificationDetail)</text>
    </binding>
  </visual>
</toast>
"@
        $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
        $xml.LoadXml($xmlContent)
        $toast = New-Object Windows.UI.Notifications.ToastNotification $xml
        [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($app_id).Show($toast)
    
        $content = @()
        $contentItem = @{
            type = "text"
            text = $Params.message
        }
        $content += $contentItem
        return @{
            content = $content
        }
    }

    # callする前にパラメータのチェックをするメソッド
    [void] Validate([object]$Params) {
        if ($null -eq $Params.message) {
            throw "パラメータ 'message' が指定されていません"
        }
    }
}


  • 作成したコマンドをaddCommand()で登録

# サーバー開始
$mcpServer = [MCPServer]::new()

# サンプルのコマンドはコメントアウト
# $mcpServer.addCommand([MCPCommandIncrement]::new())
# $mcpServer.addCommand([MCPCommandDecrement]::new())

# 作成したコマンドクラスを追加
$mcpServer.addCommand([MCPCommandNotify]::new())
$mcpServer.run()


プロンプトに以下のような文言でMCPをテストする

このフォルダの中身を確認して、一番サイズの大きいファイルをおしえて。結果はmcpのnofifyで通知してください

処理が完了したあとに通知を呼びます。

noify_call.png

トーストが表示されました。
CLIをバックグラウンドで作業させている場合でも完了を確認しやすくなりました。

notify.png

300行程度の短いソースなので解説は載せません。
余計な処理が無いのでソースを読めばMCPの実装に何が必要なのかは理解できると思います。
解説が必要であればAIにサマリしてもらってください。

おわりに(+制限事項)

学習目的のサンプルなので本記事ベースのスクリプトを一般公開するのはオススメできません。(セキュリティ的に)
ローカルにさくっと自作のMCPサーバーを作りたい方には使いやすいサンプルになっていると思います。

対応していないコマンドや、エラー系も適当なのでうまく動作しないクライアントもあるかもです。

stdio通信とjsonパーサーがあればどの言語でも作れますので、みなさんの得意の言語で作ってみると良いと思います。(sh職人やvbs職人の方の技も見てみたいです。)

Pythonが使える環境であればFastMCPを使ったほうが楽だとは思います。

私PowerShellだけど…シリーズ

私PowerShellだけど、君のタスクトレイで暮らしたい
私PowerShellだけど「送る」からファイルを受け取りたい(コンテキストメニュー登録もあるよ)
私powershellだけどタスクトレイの片隅でアイを叫ぶ
私PowerShellだけど子を持つ親になるのはいろいろ大変そう
私PowerShellだけどあなたにトーストを届けたい(プログレスバー付)
私Powershellだけど日付とサイズでログを切り替えたい(log4net)
私PowerShellだけどスクリプトだけでサービス登録したい
私PowerShellだけどスクリーンショットを撮影したい
私PowerShellだけどマウスカーソル付きのスクリーンショットを撮影したい
私PowerShellだけどスリープ抑制したい

サンプルソース置き場

1
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?