Skip to content

feat(skills): add controller-integration skill for mobile and extension#62

Merged
NicolasMassart merged 2 commits into
mainfrom
MCWP-471-create-controller-integration-agent-skill
Jul 7, 2026
Merged

feat(skills): add controller-integration skill for mobile and extension#62
NicolasMassart merged 2 commits into
mainfrom
MCWP-471-create-controller-integration-agent-skill

Conversation

@NicolasMassart

@NicolasMassart NicolasMassart commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Description

Adds the controller-integration skill: a base skill.md plus repo overlays for MetaMask Mobile (Engine) and MetaMask Extension (background messenger-client init), based on verified read-only exploration of both codebases.

Fixes https://consensyssoftware.atlassian.net/browse/MCWP-471

Type of Change

  • New skill

Skill Details (if adding a new skill)

Provider Name: MetaMask
Skill Name: controller-integration
Brief Description: Wire a new or existing controller into the MetaMask Mobile Engine or the MetaMask Extension background — init function, messenger, delegation, state persistence, feature gating, and verification steps for each repo.

Checklist

  • I have read the CONTRIBUTING.md guidelines
  • My skill follows the SKILL_TEMPLATE.md format
  • I have tested this skill with an AI agent
  • My skill does not contain any secrets, private keys, or sensitive data
  • I have added appropriate documentation
  • My changes don't break existing skills

Testing

  • metamask-skills.mjs describe coding/controller-integration --repo metamask-extensionrepo match: yes
  • metamask-skills.mjs describe coding/controller-integration --repo metamask-mobilerepo match: yes
  • node --test test/*.test.mjs → 36/36 pass

Additional Context

Example of agent chat using the skill

Mobile (Sonnet 4.6 medium)


I want to implement a controller, how should I do?


Here is the full process to integrate a controller into the MetaMask Mobile Engine.


Path decision

First, determine which path applies to your controller:

Path When
Modular init (default) New stateful controller
Stateless service No persisted state
Wallet-owned AccountsController, KeyringController, NetworkController, etc. — use this.#wallet.getInstance('<Name>')

Then, decide the source of the class:

Source When
npm package yarn add @metamask/<name>-controller
Repo-local Create app/core/Engine/controllers/<name>-controller/ with a class, types.ts, and default state

Canonical examples to copy from

  • config-registry-controller-init.ts + its messenger — standard npm-package pattern
  • card-controller/ — repo-local pattern
  • money-account-upgrade-controller-init.ts — cross-controller delegation and remote-flag gating
  • rewards-controller/ — repo-local + remote-flag gating

Steps (in order)

  1. Add the package (yarn add) or create the local controller folder
  2. Create the messenger in app/core/Engine/messengers/<name>-controller-messenger.ts — use getXControllerMessenger with messenger.delegate(...) for every allowed action/event
  3. Write a messenger testexpect(messenger).toBeInstanceOf(Messenger)
  4. Add to MESSENGER_FACTORIES in app/core/Engine/messengers/index.ts
  5. Update app/core/Engine/types.ts — add to GlobalActions, GlobalEvents, MessengerClients, EngineState, MessengerClientsToInitialize
  6. Create the init functionMessengerClientInitFunction returning { controller }
  7. Write an init test — use buildMessengerClientInitRequestMock
  8. Add the state-change event to BACKGROUND_STATE_CHANGE_EVENT_NAMES in constants.ts (⚠️ check whether the package uses :stateChange or :stateChanged)
  9. Wire into Engine.ts — import init fn, add to initFunctions after its dependencies, add to this.context, add to get state()
  10. Update app/util/test/initial-background-state.json with the default state
  11. Create a selector in app/selectors/<name>.tsstate.engine.backgroundState.<Name> ?? getDefault<Name>State()
  12. Add to .github/CODEOWNERS
  13. If remote-flag gated, register the flag key in tests/feature-flags/feature-flag-registry.ts

Verify your wiring

yarn jest app/core/Engine/Engine.test.ts -t 'matches initial state fixture'
yarn tsc --noEmit -p tsconfig.json

Common silent failures (no type error)

  • Missing event in BACKGROUND_STATE_CHANGE_EVENT_NAMES → Redux state goes stale after init
  • Wrong event name (:stateChange vs :stateChanged) → same symptom
  • Controller missing from get state() → Redux gets undefined for that key
  • Non-delegated action called at runtime → throw from @metamask/messenger

Extension (Sonnet 4.6 medium)


I want to implement a controller, how should I do?


Here's a summary of the key steps to implement a controller in this codebase:


1. File structure

Create a folder under app/scripts/controllers/your-controller/:

  • your-controller.ts — main controller file
  • your-controller.test.ts — colocated tests
  • types.ts — TypeScript types and messenger types

2. Define types and messenger

// types.ts
import type { ControllerGetStateAction, ControllerStateChangeEvent, RestrictedControllerMessenger } from '@metamask/base-controller';

export type YourControllerState = {
  items: Item[];
};

export type YourControllerGetStateAction = ControllerGetStateAction<'YourController', YourControllerState>;
export type YourControllerActions = YourControllerGetStateAction;

export type YourControllerStateChangeEvent = ControllerStateChangeEvent<'YourController', YourControllerState>;
export type YourControllerEvents = YourControllerStateChangeEvent;

type AllowedActions = NetworkControllerGetStateAction; // whatever you need
type AllowedEvents = NetworkControllerStateChangeEvent;

export type YourControllerMessenger = RestrictedControllerMessenger<
  'YourController',
  YourControllerActions | AllowedActions,
  YourControllerEvents | AllowedEvents,
  AllowedActions['type'],
  AllowedEvents['type']
>;

3. Define metadata and default state

// your-controller.ts
const yourControllerMetadata = {
  items: {
    anonymous: true,          // no PII, safe for Sentry
    includeInStateLogs: true,
    persist: true,
    usedInUi: true,
  },
};

export function getDefaultYourControllerState(): YourControllerState {
  return { items: [] }; // always a function, never a plain object export
}

4. Implement the controller class

export class YourController extends BaseController<
  'YourController',
  YourControllerState,
  YourControllerMessenger
> {
  constructor({
    messenger,
    state = {},
  }: {
    messenger: YourControllerMessenger;
    state?: Partial<YourControllerState>;
  }) {
    super({
      name: 'YourController',
      metadata: yourControllerMetadata,
      messenger,
      state: { ...getDefaultYourControllerState(), ...state },
    });

    // Subscribe to other controllers via messenger
    this.messagingSystem.subscribe('NetworkController:stateChange', this.#handleNetworkChange.bind(this));
  }

  // High-level action methods, not setters
  addItem(item: Item): void {
    if (!item.id) throw new Error('Item id is required');
    this.update((state) => {
      state.items.push(item);
    });
  }

  removeItem(id: string): void {
    this.update((state) => {
      state.items = state.items.filter((i) => i.id !== id);
    });
  }

  #handleNetworkChange(networkState: NetworkControllerState): void {
    // react to network changes
  }

  destroy(): void {
    // clean up intervals, etc.
    super.destroy();
  }
}

5. Export selectors (not getters)

import { createSelector } from 'reselect';

const selectItems = (state: YourControllerState) => state.items;

export const yourControllerSelectors = {
  selectItems,
  selectItemCount: createSelector([selectItems], (items) => items.length),
};

Key rules to remember

Rule Why
this.update() only, never direct mutation Immer-based, ensures immutability
Single options bag in constructor Consistency across the codebase
Export getDefault...State() function, not object Prevents shared reference mutations
No getters — use selectors Reusable outside the controller, memoizable
Messenger for inter-controller communication Decoupled, testable
Methods model user actions, not property setters Clear intent

Once you're done, if you also need to wire the controller into the extension, let me know and I can guide you through the integration step (connecting it to metamask-controller.ts, the messenger client, and Redux).


Adds the verified metamask-extension repo overlay for the
controller-integration skill and generalizes the base skill
description to cover both mobile Engine and extension background
integration paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
@NicolasMassart NicolasMassart self-assigned this Jul 2, 2026
@NicolasMassart NicolasMassart added the enhancement New feature or request label Jul 2, 2026
@NicolasMassart NicolasMassart requested a review from Copilot July 2, 2026 17:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new controller-integration skill under domains/coding/skills/ to guide integrating controllers into MetaMask Mobile (Engine) and MetaMask Extension (background messenger-client init), with repo-specific overlays.

Changes:

  • Adds the base controller-integration skill definition (skill.md).
  • Adds a MetaMask Mobile overlay documenting the Engine controller integration workflow and verification steps.
  • Adds a MetaMask Extension overlay documenting background messenger-client init integration steps, including state/persistence and safety checks.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
domains/coding/skills/controller-integration/skill.md Defines the new skill and when to use it.
domains/coding/skills/controller-integration/repos/metamask-mobile.md Mobile-specific controller integration steps for Engine (messengers, init, state, verification).
domains/coding/skills/controller-integration/repos/metamask-extension.md Extension-specific background integration steps for messenger-client init (init map, state flattening, filtering, verification).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@NicolasMassart NicolasMassart changed the title feat(controller-integration): add metamask-extension overlay feat(skills): add controller-integration skill for mobile and extension Jul 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@Cal-L Cal-L left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@NicolasMassart NicolasMassart merged commit fad21fb into main Jul 7, 2026
28 checks passed
@NicolasMassart NicolasMassart deleted the MCWP-471-create-controller-integration-agent-skill branch July 7, 2026 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants