Build with AI

WebMCP

Web Model Context Protocol (WebMCP)は、Webアプリケーションがブラウザ内でネイティブに実行されるAIエージェントに構造化されたツールを直接公開できるようにする新興のWeb標準です。アプリケーションによって定義されたツールにより、AIアシスタントはアプリケーションと直接対話できるようになり、エージェントに追加の機能を提供し、DOM操作の必要性を減らします。

例えば、新規ユーザーを登録するアプリケーションは、エージェントがDOM操作を通じて複雑なウィザードUIを操作することを要求する代わりに、ブラウザのAIエージェントがユーザーを直接作成するためのWebMCPツールを提供する場合があります。

AngularはWebMCPの実験的サポートを提供しており、アプリケーションの依存性の注入ライフサイクルに結びついたツールを簡単に登録し、シグナルフォームを自動的にAI対応ツールに変換できます。

IMPORTANT: WebMCP仕様はライフサイクルの非常に初期の段階にあり、頻繁に変更されています。そのため、AngularにおけるWebMCPサポートは現在実験的です。APIはメジャーバージョン以外でも変更される可能性があります。

アプリケーションにツールを提供する

アプリケーション設定でprovideExperimentalWebMcpToolsを使用して、アプリケーションのライフサイクル全体にわたってツールを登録します。この方法で提供されたツールは、アプリケーションの初期化時に自動的に登録され、アプリケーションの破棄時に登録解除されます。

executeコールバックは関連付けられたInjectorの注入コンテキストで呼び出されるため、サービスを直接injectできます。

main.ts

import {Service, inject, provideExperimentalWebMcpTools} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
import {AppRoot} from './app-root';

@Service()
class Greeter {
  sayHello(): string {
    return 'Hello agent!';
  }
}

bootstrapApplication(AppRoot, {
  providers: [
    provideExperimentalWebMcpTools([
      {
        name: 'greet',
        description: 'Greets the agent.',
        inputSchema: {type: 'object', properties: {}},
        execute: () => {
          const greeter = inject(Greeter);

          return {content: [{type: 'text', text: greeter.sayHello()}]};
        },
      },
    ]),
  ],
});

ツールパラメータを定義する

ツールがAIアシスタントからの入力を必要とする場合、JSON Schema構文を使用してinputSchema内に期待される引数を定義します。Angularはスキーマ定義に基づいて、executeコールバックに渡されるパラメータの型を自動的に推論します。

main.ts

import {provideExperimentalWebMcpTools} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
import {AppRoot} from './app-root';

bootstrapApplication(AppRoot, {
  providers: [
    provideExperimentalWebMcpTools([
      {
        name: 'searchCatalog',
        description: 'Searches the store catalog for products matching a query.',
        inputSchema: {
          type: 'object',
          properties: {
            query: {
              type: 'string',
              description: 'The search keywords.',
            },
            maxResults: {
              type: 'number',
              description: 'Maximum number of results to return.',
            },
          },
          required: ['query'],
          additionalProperties: false,
        },
        execute: ({query, maxResults}) => {
          // Type of `query` is inferred as `string`.
          // Type of `maxResults` is inferred as `number | undefined`.

          // Consider validating this at runtime, since inputs may not be validated to match the schema.
          if (typeof query !== 'string') throw new Error(`Bad query: ${query}`);
          if (typeof maxResults !== 'number' && maxResults !== undefined)
            throw new Error(`Bad maxResults: ${maxResults}`);

          const limit = maxResults ?? 5;
          return {
            content: [{type: 'text', text: `Returning up to ${limit} results for "${query}".`}],
          };
        },
      },
    ]),
  ],
});

TIP: required: ['param1', 'param2', ...]を使用してそれらのパラメータの型からundefinedを削除し、additionalProperties: falseを使用して引数オブジェクトの型をこれらのパラメータのみに制限します。

ルートにツールを提供する

複雑なアプリケーションを構築する場合、ユーザーが特定のルートを表示しているときにのみ特定のツールを利用できるようにしたい場合があります。これは、ルート定義で直接ツールを提供することで実現できます。

routes.ts

import {provideExperimentalWebMcpTools} from '@angular/core';
import {Routes} from '@angular/router';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard').then((m) => m.Dashboard),
    providers: [
      provideExperimentalWebMcpTools([
        {
          name: 'exportDashboardReports',
          description: 'Exports the current dashboard analytics.',
          inputSchema: {type: 'object', properties: {}},
          execute: () => ({
            content: [{type: 'text', text: 'Dashboard export successfully triggered.'}],
          }),
        },
      ]),
    ],
  },
];

NOTE: 特定のルートにツールを登録する場合、ユーザーがルートから移動したときにツールが自動的に_登録解除_されるように、ルーターを構成してwithExperimentalAutoCleanupInjectorsを使用することを検討してください。このオプションがない場合、ルートで宣言されたWebMCPツールは、ユーザーが別のルートに移動した後でもAIエージェントからアクセス可能なままになります。

app.config.ts

import {ApplicationConfig} from '@angular/core';
import {provideRouter, withExperimentalAutoCleanupInjectors} from '@angular/router';
import {routes} from './routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes, withExperimentalAutoCleanupInjectors())],
};

サービス内でのツールの提供

動的なユースケースの場合、declareExperimentalWebMcpTool関数は注入コンテキスト内にツールを直接登録し、そのコンテキストが破棄されたときに自動的に登録を解除します。

counter.ts

import {Service, declareExperimentalWebMcpTool, signal, inject} from '@angular/core';

@Service()
export class Counter {
  readonly count = signal(0);

  constructor() {
    declareExperimentalWebMcpTool({
      name: 'getCounter',
      description: 'Reads the global counter.',
      inputSchema: {type: 'object', properties: {}},
      execute: () => ({
        content: [{type: 'text', text: `The count is: ${this.count()}.`}],
      }),
    });
  }
}

declareExperimentalWebMcpToolは任意の注入コンテキストで機能しますが、名前の衝突に注意し、ルートサービスで使用することを推奨します。

シグナルフォームの暗黙的ツール

最小限の設定で、既存のAngularのSignal Formから暗黙的にWebMCPツールを作成できます。AngularはフォームモデルをリッチなWebMCPツールに変換し、JSONスキーマやイベントハンドラーを手動で記述することなく、高度に動的なフォームを効果的にサポートします。

WebMCPフォーム機能の有効化

まず、ルートアプリケーションのプロバイダーにprovideExperimentalWebMcpFormsを追加します:

main.ts

import {bootstrapApplication} from '@angular/platform-browser';
import {provideExperimentalWebMcpForms} from '@angular/forms/signals';
import {AppRoot} from './app-root';

bootstrapApplication(AppRoot, {
  providers: [provideExperimentalWebMcpForms()],
});

Signal Formのオプトイン

次に、formを使用してSignal Formを定義する際、experimentalWebMcpTool設定オプションを渡して暗黙的なWebMCPツールにオプトインします。Angularはフォームのデータモデルを検査し、接続されたAIエージェント用のJSONスキーマを自動的に生成します。

user-registration.ts

import {Component, signal} from '@angular/core';
import {form, required, minLength} from '@angular/forms/signals';

@Component({
  selector: 'app-user-registration',
  templateUrl: './user-registration.html',
})
export class UserRegistration {
  private readonly model = signal({
    firstName: '',
    lastName: '',
    age: 0,
    hobbies: ['Web Development'],
  });

  readonly userForm = form(
    this.model,
    (f) => {
      required(f.firstName, {message: 'First name is mandatory.'});
      required(f.lastName, {message: 'Last name is mandatory.'});
    },
    {
      // Implicitly registers a WebMCP tool named `registerUser` with parameters derived from `model`.
      experimentalWebMcpTool: {
        name: 'registerUser',
        description: 'Registers a new user.',
      },
      submission: {
        action: async (formValue) => {
          console.log('Submitting user:', formValue);
          // ...
        },
      },
    },
  );
}

この例では、Angularは以下のJSONスキーマを持つWebMCPツールを生成します:

  1. modelシグナルの初期値から推論されたパラメーターとして、firstNamelastNameage、およびhobbiesを含みます。
  2. requiredバリデーターから推論された_必須_フィールドとして、firstNamelastNameを定義します。
  3. hobbiesを文字列の配列として定義し、エージェントが任意の数の趣味を提供できるようにします。

入力スキーマの推論にとどまらず、AngularはWebMCPツールをフォームの検証ロジックと送信ハンドラーにも接続します。これにより、エージェントは自身の入力によってトリガーされた検証エラーや送信中に発生した失敗を監視し、自己修正して再試行できるようになります。

NOTE: 非同期バリデーターはトリガーされ_ない_ため、送信アクションで処理する必要があります。

制約事項

Angularはフォームモデルの初期値からWebMCPスキーマを推論します。これには以下が必要です:

  • 具体的な初期値(''0false): Angularはnullundefinedからデータ型を推論できません。
  • 空ではない配列(['Hello!']): Angularは空の配列からデータ型を推論できず、少なくとも1つの初期値を必要とします。

ベストプラクティス

以下のベストプラクティスを念頭に置いてください:

名前の衝突

WebMCPは各ツールが一意の名前を持つことを要求し、同じツール名が複数回登録された場合はエラーをスローします。これは、複数回登録される可能性のあるコンテキスト(コンポーネントのコンストラクターなど)でdeclareExperimentalWebMcpToolprovideExperimentalWebMcpToolsを呼び出すと、実行時にエラーが発生する可能性があることを意味します。

可能な限り、ツールはアプリケーションプロバイダー、ルートプロバイダー、またはルートサービスに配置することを推奨します。Signal Formsの暗黙的ツールを含め、コンポーネントにツールを配置する場合は、そのコンポーネントが常にページ上で最大_1回_しかレンダリングされないことを確認してください。

ツール入力の検証

Angularは、エージェントによって提供された入力が定義されたJSONスキーマと実際に一致するかどうかの暗黙的な検証を提供しません。信頼性を確保するために、使用する前にexecute関数への引数を明示的に検証することを検討してください。

テスト

ツールを効果的にユニットテストするために、@mcp-b/webmcp-polyfillのようなモックWebMCP実装を使用することを検討してください。