NeovimからPlaywright MCPでブラウザ操作

この記事では、avante.nvimにPlaywrightのMCPを導入してブラウザを自動操作する方法を紹介します。

github.com

MCPクライアントのインストール

Neovim用MCPクライアントのmcphub.nvimをインストールします。

github.com

lazy.nvimの設定ファイルに以下を追加しました。
インストール手順の詳細についてはこちらを参照してください。

{
  "ravitemer/mcphub.nvim",
  dependencies = {
    "nvim-lua/plenary.nvim",
  },
  build = "npm install -g mcp-hub@latest",
  config = function()
    require("mcphub").setup()
  end
},

Playwright MCPのインストール

microsoft/playwright-mcpをインストールします。

github.com

MCP HubではマーケットプレイスからMCPサーバをインストールできますが、 Playwright用はexecuteautomation/mcp-playwrightしか見つかりませんでした。
そこで、 ~/.config/mcphub/servers.json を直接編集してMicrosoft版を追加します。

以下の設定をファイルに追加してPlaywright MCPサーバを登録します。

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest"]
    }
  }
}

:MCPHubMCPサーバにPlaywrightが追加されたことを確認します。

ツール実行の度に承認するのは手間なので、 <a> でAuto-approve(自動承認)を設定しておきます。

動作確認

実際にPlaywright MCPが正常に動作するか確認します。
ブラウザを開いて Playwright と検索する指示を出してみました。

指示に従ってブラウザが自動で開きます。

検索ボックスに「Playwright」と文字列が自動入力されます。

検索が実行され、Playwrightに関する検索結果が表示されました。

さいごに

avante.nvimにPlaywright MCPを導入することで、AIアシスタントがブラウザを直接操作できるようになりました。今回の例では簡単な検索操作でしたが、Playwrightの豊富な機能を活用すれば、より複雑なWeb操作の自動化も可能です。

MCPを活用することで、従来のAIチャットだけでは実現できなかった「実際の操作を伴うタスク」をAIに任せることができます。これにより、開発者の生産性向上や作業の自動化が期待できるでしょう。

Playwright MCP以外にも様々なMCPサーバが公開されているので、用途に応じて組み合わせることで、さらに便利な開発環境を構築できそうです。

DocumentDB 5.0のダンプをMongoDBにリストア失敗:「documentDB is not a registered storage engine」の原因と対策

はじめに

DocumentDB 5.0(以下、DocDB)のダンプを MongoDB 5.0 にリストアしようとしたところ、以下のエラーが発生しました。

  • DocDB 5.0 のバックアップ
$ mongodump --uri="mongodb://..." --gzip --archive=/mydb.gz ...
  • MongoDB 5.0 にリストア
$ mongorestore \
  --username <username> \
  --password <password> \
  --authenticationDatabase admin \
  --nsInclude 'mydb.*' \
  --gzip \
  --archive=/mydb.gz \
  --drop
...

Failed: ... (InvalidOptions) documentDB is not a registered storage engine for this server

DocDB 4.0 では問題なくリストアできていたため、バージョンアップに伴う変更が原因と考えられます。

原因

DocDB 5.0 で追加されたドキュメントの圧縮機能が原因でした。
本機能の使用有無によらず DocDB 独自の storageEngine がダンプに含まれるため、MongoDB でリストアするとエラーが発生します。

storageEngine: {
  documentDB: {compression: {enable: <true | false>} }
}

対策

こちらの記事のようにメタデータを直接編集する方法もありますが、 コレクションのオプションを無視してよければ--noOptionsRestoreオプションを指定することでエラーを回避できます。

$ mongorestore --noOptionsRestore ...
...
6485341 document(s) restored successfully. 0 document(s) failed to restore.

Zero to JupyterHub with Kubernetes(Z2JH)でユーザデータをS3にバックアップ

背景

EKSでZero to JupyterHub with Kubernetes(Z2JH)で構築したJupyterHubを運用しています。
障害時の回復力を高めるため、AWS外のストレージ(Cloud Storage等)にバックアップの複製を作成することになり、Storage Transfer ServiceRcloneの利用が可能なS3にユーザデータ(ノートブック等)のバックアップを取得する方式を構築しました。

実行環境

  • EKS 1.25
  • Z2JH 3.2.0

IAM ServiceAccountの作成

ノートブックサーバにS3バケットへの書き込み権限を与えるサービスアカウント singleuser を作成します。

$ BUCKET_NAME=<bucket-name>
$ POLICY_FILE=$(mktemp -t s3.iam.policy.json)
$ cat<<EOF> ${POLICY_FILE}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::${BUCKET_NAME}/*"
      ]
    }
  ]
}
EOF

$ POLICY_ARN=$(
    aws iam create-policy \
      --policy-name AmazonS3BucketWriteOnlyAccess \
      --policy-document file://${POLICY_FILE} \
      --query 'Policy.Arn' \
      --output text
  )

$ CLUSTER_NAME=<cluster-name>
$ NAMESPACE=<namespace>
$ eksctl create iamserviceaccount \
    --cluster=${CLUSTER_NAME} \
    --namespace=${NAMESPACE} \
    --name=singleuser \
    --attach-policy-arn=${POLICY_ARN} \
    --approve

Z2JHの設定

チャートの values.yaml にバックアップ処理を追加します。

singleuser.serviceAccountNameでServiceAccount singleuser を指定し、 バケットへの書き込み権限を付与します。

singleuser:
  serviceAccountName: singleuser

singleuser.lifecycleHooks.preStopでサーバの停止時にS3にユーザデータをコピーする処理を追加します。

singleuser:
  lifecycleHooks:
    preStop:
      exec:
        command:
          - sh
          - -c
          - >
            tar -C /home/jovyan -czf .${JUPYTERHUB_USER}.tar.gz .;
            aws s3 cp .${JUPYTERHUB_USER}.tar.gz s3://<bucket-name>/jhub/singleuser/${JUPYTERHUB_USER}.tar.gz; # <bucket-name>にバケット名を指定
            rm .${JUPYTERHUB_USER}.tar.gz;

preStop の処理が中断されないようにhub.extraConfigc.KubeSpawner.delete_grace_periodを設定し、Pod削除までの時間を延長します。

hub:
  extraConfig:
    kubeSpawnerConfig:
      c.KubeSpawner.delete_grace_period = 30

バックアップの確認

これでノートブックサーバの停止を契機に、バケットにバックアップファイルが作成されるようになりました。

kinitを使わずにPython-GSSAPIでKeytabを指定してTGTを発行

kinit を使わずに済む方法を探していたところ、以下の実装で動きました。
gssapi.Credentials.storeclient_keytabにKeytabファイルのパスを指定します。

import gssapi

gssapi.Credentials(
    usage='initiate',
    name=gssapi.Name(
        '<SERVER_PRINCIPAL_NAME>',
        gssapi.NameType.kerberos_principal
    ),
    store={'client_keytab': '/path/to/keytab'}
)

<SERVER_PRINCIPAL_NAME>klist -k /path/to/keytab で取得できる値です。

カレンダーを和暦にするとDuolingoのデイリークエストができない話

Duolingoで韓国語を勉強しているんですが、なぜかiPhoneだとデイリークエストができない問題に悩んでました。

問題を回避するため、iPad版やブラウザ版をプレイすることでバッジを取得していたんですが、いい加減面倒に感じていたところ、Redditでこんな記事を発見。

Cant see daily quest
byu/ocean_008 induolingo
> somehow am in 1480
>> That'll be the problem right there. Daily quests won't be introduced until 2022, so you'll need to wait 542 years.
>>> ... i just change the calendar and everything work fine now.

カレンダーの問題っぽいと。確認してみると、なんと4042年を生きていたようです。
二千年後にデイリークエストが廃止されていても不思議はありません。

iOSのカレンダー設定をみると和暦表示になっていました。
これを西暦(グレゴリオ暦)に変えてあげると...

直りました!

3年ぐらい悩んでたんで、感動が大きいです。
てっきりA/Bテストだと思って諦めてたのにこんな簡単な話だったとは...
一向に修正されない様子をみると、発生条件が他にもあるのかもしれません(言語設定を英語にしているとか)。

DRFのDecimalFieldから末尾のゼロを除外

Django REST framework (DRF) において、DecimalFieldから末尾のゼロを除去する方法です。具体的には以下のように挙動を変更します。

# 現在
Decimal('0.001000000') >> "0.001000000"
# 期待する結果
Decimal('0.001000000') >> "0.001"

この機能は以下のPRで追加されましたが、まだリリースされていません。

github.com

https://github.com/Krolken/django-rest-framework/blob/92bcdd021a51d047836c6fdfdc4b981930717131/docs/api-guide/fields.md#decimalfield

・ normalize_output
Will normalize the decimal value when serialized. This will strip all trailing zeroes and change the value's precision to the minimum required precision to be able to represent the value without loosing data. Defaults to False.

また、この機能は DecimalField(normalize_output=True) で有効になるため、全体への適用には少々手間がかかります。
そこで、settingsの拡張を提案しましたが、現在DRFはメンテナンスフェーズ*1にあるため、提案は採用されませんでした。

github.com

そこで、#6514の変更を参考に、 quantized.normalize() を追加したカスタムフィールドを作成しました。
このカスタムフィールドを ModelSerializer.serializer_field_mappingmodels.DecimalFieldマッピングすることで、カスタムフィールドを標準適用します。
(COERCE_DECIMAL_TO_STRING関連の処理は使わないので削っています。)

import decimal

from django.db import models
from django.utils.formats import localize_input
from rest_framework import serializers


class NormalizedDecimalField(serializers.DecimalField):
    def to_representation(self, value):
        if value is None:
            return ''

        if not isinstance(value, decimal.Decimal):
            value = decimal.Decimal(str(value).strip())

        quantized = self.quantize(value)

        quantized = quantized.normalize()

        if self.localize:
            return localize_input(quantized)

        return '{:f}'.format(quantized)


class CustomModelSerializer(serializers.ModelSerializer):
    serializer_field_mapping = (
        serializers.ModelSerializer.serializer_field_mapping.copy()
    )
    serializer_field_mapping[models.DecimalField] = NormalizedDecimalField

あとはシリアライザの基底クラスを CustomModelSerializer に変更するだけで、各DecimalFieldから末尾のゼロを除外できます。

- class MySerializer(serializers.ModelSerializer):
+ class MySerializer(CustomModelSerializer):

モデルのプロパティがDecimal値を返す場合には、シリアライザクラスに NormalizedDecimalField を明示的に定義してください。

class MySerializer(CustomModelSerializer):
    my_property = NormalizedDecimalField(max_digits=..., decimal_places=...)

EKSのマネージド型ノードを並列更新

ノードを並列更新することで、マネージド型ノードグループの更新時間を短縮できるようなので試してみました。

docs.aws.amazon.com

It determines the maximum quantity of nodes to upgrade in parallel using the updateConfig property for the node group.

aws.amazon.com

Now, the amount of nodes that can be upgraded at a time is configurable, and clusters with applications that are more fault tolerant can benefit from reduced time to complete node group upgrades.

ノードグループの設定から「Node Group update configuration」で「Number」または「Percentage」に並列更新するノードの数または割合を設定します(ただし、並列更新できるのは最大で100ノード)。
今回はPercentageを100%とし、一度に全ノードを更新します。

docs.aws.amazon.com

eksctlのYAMLで管理する場合はこちらを参照してください。

managedNodeGroups:
...
    updateConfig:
      maxUnavailablePercentage: 100

設定変更後、試しにAMIを更新してみると、複数のノードが一度に更新される様子を確認できました。

❯ eksctl upgrade nodegroup \
   --name=<node-group-name> \
   --cluster=<cluster-name> \
   --region=<region>
...
❯ kubectl get nodes 
NAME                                                STATUS                     ROLES    AGE     VERSION
ip-192-168-14-112.ap-northeast-1.compute.internal   Ready                      <none>   3m34s   v1.24.15-eks-a5565ad
ip-192-168-18-140.ap-northeast-1.compute.internal   Ready,SchedulingDisabled   <none>   5d21h   v1.24.15-eks-a5565ad
ip-192-168-24-91.ap-northeast-1.compute.internal    Ready,SchedulingDisabled   <none>   5d21h   v1.24.15-eks-a5565ad
ip-192-168-27-53.ap-northeast-1.compute.internal    Ready                      <none>   3m31s   v1.24.15-eks-a5565ad
ip-192-168-32-111.ap-northeast-1.compute.internal   Ready                      <none>   3m34s   v1.24.15-eks-a5565ad
ip-192-168-44-222.ap-northeast-1.compute.internal   Ready                      <none>   3m29s   v1.24.15-eks-a5565ad
ip-192-168-45-236.ap-northeast-1.compute.internal   Ready,SchedulingDisabled   <none>   5d20h   v1.24.15-eks-a5565ad
ip-192-168-53-167.ap-northeast-1.compute.internal   Ready,SchedulingDisabled   <none>   5d21h   v1.24.15-eks-a5565ad
ip-192-168-64-207.ap-northeast-1.compute.internal   Ready                      <none>   3m34s   v1.24.15-eks-a5565ad
ip-192-168-83-46.ap-northeast-1.compute.internal    Ready                      <none>   3m27s   v1.24.15-eks-a5565ad
ip-192-168-88-250.ap-northeast-1.compute.internal   Ready,SchedulingDisabled   <none>   5d21h   v1.24.15-eks-a5565ad
ip-192-168-90-162.ap-northeast-1.compute.internal   Ready,SchedulingDisabled   <none>   5d20h   v1.24.15-eks-a5565ad

なお、スケールダウンは相変わらず1ノードずつ処理するので時間がかかるとのことです。

github.com