diff --git a/packages/example-plugin2/fixture.js b/packages/example-plugin2/fixture.js
new file mode 100644
index 00000000..d72b480b
--- /dev/null
+++ b/packages/example-plugin2/fixture.js
@@ -0,0 +1 @@
+module.exports = 'you bet!';
diff --git a/packages/example-plugin2/index.js b/packages/example-plugin2/index.js
new file mode 100644
index 00000000..6755ddd9
--- /dev/null
+++ b/packages/example-plugin2/index.js
@@ -0,0 +1,30 @@
+const required = require('./fixture');
+const neovim = require('neovim');
+
+let nvim;
+
+function hostTest(args, range) {
+  if (args[0] === 'canhazresponse?') {
+    throw new Error('no >:(');
+  }
+
+  nvim.setLine('A line, for your troubles');
+
+  return 'called hostTest';
+}
+
+function onBufEnter(filename) {
+  return new Promise((resolve, reject) => {
+    console.log('This is an annoying function ' + filename);
+    resolve(filename);
+  });
+}
+
+function main() {
+  nvim = neovim.cli();
+  // Now that we successfully started, we can remove the default listener.
+  //nvim.removeAllListeners('request');
+  nvim.setHandler('testMethod1', hostTest);
+}
+
+main();
diff --git a/packages/example-plugin2/package.json b/packages/example-plugin2/package.json
new file mode 100644
index 00000000..04e8020f
--- /dev/null
+++ b/packages/example-plugin2/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "@neovim/example-plugin2",
+  "private": true,
+  "version": "1.0.0",
+  "description": "Test fixture for new rplugin design",
+  "main": "index.js",
+  "license": "MIT",
+  "devDependencies": {}
+}
diff --git a/packages/integration-tests/__tests__/integration.test.ts b/packages/integration-tests/__tests__/integration.test.ts
index 47231301..126cee1c 100644
--- a/packages/integration-tests/__tests__/integration.test.ts
+++ b/packages/integration-tests/__tests__/integration.test.ts
@@ -4,9 +4,16 @@ import * as fs from 'fs';
 import * as path from 'path';
 import * as http from 'http';
 
+//
+//
+// TODO: The old rplugin design is deprecated and NOT supported.
+//       This file will be deleted.
+//
+//
+
 import { NeovimClient, attach, findNvim } from 'neovim';
 
-describe('Node host', () => {
+describe.skip('Node host (OLD, DELETE ME)', () => {
   const testdir = process.cwd();
   let proc: cp.ChildProcessWithoutNullStreams;
   let args;
diff --git a/packages/integration-tests/__tests__/rplugin2.test.ts b/packages/integration-tests/__tests__/rplugin2.test.ts
new file mode 100644
index 00000000..f226d047
--- /dev/null
+++ b/packages/integration-tests/__tests__/rplugin2.test.ts
@@ -0,0 +1,127 @@
+/* eslint-env jest */
+import * as cp from 'child_process';
+import * as path from 'path';
+
+import { NeovimClient, attach, findNvim } from 'neovim';
+
+/**
+ * Runs a program and returns its output.
+ */
+async function run(cmd: string, args: string[]) {
+  return new Promise<{ proc: ReturnType<typeof cp.spawn>, stdout: string, stderr: string}>((resolve, reject) => {
+    const proc = cp.spawn(cmd, args, { shell: false });
+    const rv = {
+      proc: proc,
+      stdout: '',
+      stderr: '',
+    }
+
+    proc.stdout.on('data', (data) => {
+      rv.stdout += data.toString();
+    });
+
+    proc.stderr.on('data', (data) => {
+      rv.stderr += data.toString();
+    });
+
+    proc.on('exit', (code_) => {
+      resolve(rv);
+    });
+
+    proc.on('error', (e) => {
+      reject(e);
+    });
+  });
+}
+
+describe('Node host2', () => {
+  const thisDir = path.resolve(__dirname);
+  const pluginDir = path.resolve(thisDir, '../../example-plugin2/');
+  const pluginMain = path.resolve(pluginDir, 'index.js').replace(/\\/g, '/');
+
+  const testdir = process.cwd();
+  let nvimProc: ReturnType<typeof cp.spawn>;
+  let nvim: NeovimClient;
+
+  beforeAll(async () => {
+    const minVersion = '0.9.5'
+    const nvimInfo = findNvim({ minVersion: minVersion });
+    const nvimPath = nvimInfo.matches[0]?.path;
+    if (!nvimPath) {
+      throw new Error(`nvim ${minVersion} not found`)
+    }
+
+    nvimProc = cp.spawn(nvimPath, ['--clean', '-n', '--headless', '--embed'], {});
+    nvim = attach({ proc: nvimProc });
+  });
+
+  afterAll(() => {
+    process.chdir(testdir);
+    nvim.quit();
+    if (nvimProc && nvimProc.connected) {
+      nvimProc.disconnect();
+    }
+  });
+
+  beforeEach(() => {});
+
+  afterEach(() => {});
+
+
+  /**
+   * From the Nvim process, starts a new "node …/plugin/index.js" RPC job (that
+   * is, a node "plugin host", aka an Nvim node client).
+   */
+  async function newPluginChan() {
+    const nodePath = process.argv0.replace(/\\/g, '/');
+    const luacode = `
+      -- "node …/plugin/index.js"
+      local argv = { [[${nodePath}]], [[${pluginMain}]] }
+      local chan = vim.fn.jobstart(argv, { rpc = true, stderr_buffered = true })
+      return chan
+    `
+    return await nvim.lua(luacode);
+  }
+
+  it('`node plugin.js --version` prints node-client version', async () => {
+    //process.chdir(thisDir);
+    const proc = await run(process.argv0, [pluginMain, '--version']);
+    // "5.1.1-dev.0\n"
+    expect(proc.stdout).toMatch(/\d+\.\d+\.\d+/);
+
+    proc.proc.kill('SIGKILL');
+  });
+
+  it('responds to "poll" with "ok"', async () => {
+    // See also the old provider#Poll() function.
+
+    // From Nvim, start an "node …/plugin/index.js" RPC job.
+    // Then use that channel to call methods on the remote plugin.
+    const chan = await newPluginChan();
+    const rv = await nvim.lua(`return vim.rpcrequest(..., 'poll')`, [ chan ]);
+
+    expect(rv).toEqual('ok');
+  });
+
+  //it('responds to "nvim_xx" methods', async () => {
+  //  // This is just a happy accident of the fact that Nvim plugin host === client.
+  //  const chan = await newPluginChan();
+  //  const rv = await nvim.lua(`return vim.rpcrequest(..., 'nvim_eval', '1 + 3')`, [ chan ]);
+  //  expect(rv).toEqual(3);
+  //});
+
+  it('responds to custom, plugin-defined methods', async () => {
+    const chan = await newPluginChan();
+    // The "testMethod1" function is defined in …/example-plugin2/index.js.
+    const rv = await nvim.lua(`return vim.rpcrequest(..., 'testMethod1', {})`, [ chan ]);
+
+    expect(rv).toEqual('called hostTest');
+  });
+
+  // TODO
+  //it('Lua plugin can define autocmds/functions that call the remote plugin', async () => {
+  //  // JSHostTestCmd
+  //  // BufEnter
+  //});
+});
+
diff --git a/packages/neovim/src/api/client.ts b/packages/neovim/src/api/client.ts
index 672dff9e..8ab1ea10 100644
--- a/packages/neovim/src/api/client.ts
+++ b/packages/neovim/src/api/client.ts
@@ -2,7 +2,7 @@
  * Handles attaching transport
  */
 import { Logger } from '../utils/logger';
-import { Transport } from '../utils/transport';
+import { Response, Transport } from '../utils/transport';
 import { VimValue } from '../types/VimValue';
 import { Neovim } from './Neovim';
 import { Buffer } from './Buffer';
@@ -12,12 +12,30 @@ const REGEX_BUF_EVENT = /nvim_buf_(.*)_event/;
 export class NeovimClient extends Neovim {
   protected requestQueue: any[];
 
+  /**
+   * Handlers for custom (non "nvim_") methods registered by the remote module.
+   * These handle requests from the Nvim peer.
+   */
+  public handlers: {
+    [index: string]: (args: any[], event: { name: string }) => any;
+  } = {};
+
   private transportAttached: boolean;
 
   private _channelId?: number;
 
   private attachedBuffers: Map<string, Map<string, Function[]>> = new Map();
 
+  /**
+   * Defines a handler for incoming RPC request method/notification.
+   */
+  setHandler(
+    method: string,
+    fn: (args: any[], event: { name: string }) => any
+  ) {
+    this.handlers[method] = fn;
+  }
+
   constructor(options: { transport?: Transport; logger?: Logger } = {}) {
     // Neovim has no `data` or `metadata`
     super({
@@ -66,7 +84,7 @@ export class NeovimClient extends Neovim {
   handleRequest(
     method: string,
     args: VimValue[],
-    resp: any,
+    resp: Response,
     ...restArgs: any[]
   ) {
     // If neovim API is not generated yet and we are not handle a 'specs' request
diff --git a/packages/neovim/src/cli.ts b/packages/neovim/src/cli.ts
new file mode 100644
index 00000000..06ea393f
--- /dev/null
+++ b/packages/neovim/src/cli.ts
@@ -0,0 +1,100 @@
+import { spawnSync } from 'node:child_process';
+import { attach } from './attach';
+
+let nvim: ReturnType<typeof attach>;
+
+// node <current script> <rest of args>
+const [, , ...args] = process.argv;
+
+if (args[0] === '--version') {
+  // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
+  const pkg = require('../package.json');
+  // eslint-disable-next-line no-console
+  console.log(pkg.version);
+  process.exit(0);
+}
+
+// "21.6.1" => "21"
+const nodeMajorVersionStr = process.versions.node.replace(/\..*/, '');
+const nodeMajorVersion = Number.parseInt(nodeMajorVersionStr ?? '0', 10);
+
+if (
+  process.env.NVIM_NODE_HOST_DEBUG &&
+  nodeMajorVersion >= 8 &&
+  process.execArgv.every(token => token !== '--inspect-brk')
+) {
+  const childHost = spawnSync(
+    process.execPath,
+    process.execArgv.concat(['--inspect-brk']).concat(process.argv.slice(1)),
+    { stdio: 'inherit' }
+  );
+  process.exit(childHost.status ?? undefined);
+}
+
+export interface Response {
+  send(resp: any, isError?: boolean): void;
+}
+
+process.on('unhandledRejection', (reason, p) => {
+  process.stderr.write(`Unhandled Rejection at: ${p} reason: ${reason}\n`);
+});
+
+/**
+ * The "client" is also the "host". https://github.com/neovim/neovim/issues/27949
+ */
+async function handleRequest(method: string, args: any[], res: Response) {
+  nvim.logger.debug('request received: %s', method);
+  // 'poll' and 'specs' are requests from Nvim internals. Else we dispatch to registered remote module methods (if any).
+  if (method === 'poll') {
+    // Handshake for Nvim.
+    res.send('ok');
+    // } else if (method.startsWith('nvim_')) {
+    //  // Let base class handle it.
+    //  nvim.request(method, args);
+  } else {
+    const handler = nvim.handlers[method];
+    if (!handler) {
+      const msg = `node-client: missing handler for "${method}"`;
+      nvim.logger.error(msg);
+      res.send(msg, true);
+    }
+
+    try {
+      nvim.logger.debug('found handler: %s: %O', method, handler);
+      const plugResult = await handler(args, { name: method });
+      res.send(
+        !plugResult || typeof plugResult === 'undefined' ? null : plugResult
+      );
+    } catch (e) {
+      const err = e as Error;
+      const msg = `node-client: failed to handle request: "${method}": ${err.message}`;
+      nvim.logger.error(msg);
+      res.send(err.toString(), true);
+    }
+  }
+}
+
+// "The client *is* the host... The client *is* the host..."
+//
+// "Main" entrypoint for any Nvim remote plugin. It implements the Nvim remote
+// plugin specification:
+// - Attaches self to incoming RPC channel.
+// - Responds to "poll" with "ok".
+// - TODO: "specs"?
+export function cli() {
+  try {
+    // Reverse stdio because it's from the perspective of Nvim.
+    nvim = attach({ reader: process.stdin, writer: process.stdout });
+    nvim.logger.debug('host.start');
+    nvim.on('request', handleRequest);
+
+    return nvim;
+  } catch (e) {
+    const err = e as Error;
+    process.stderr.write(
+      `failed to start Nvim plugin host: ${err.name}: ${err.message}\n`
+    );
+
+    return undefined;
+  }
+}
diff --git a/packages/neovim/src/host/index.ts b/packages/neovim/src/host/index.ts
index a19fbfe8..c88f3be7 100644
--- a/packages/neovim/src/host/index.ts
+++ b/packages/neovim/src/host/index.ts
@@ -6,6 +6,9 @@ export interface Response {
   send(resp: any, isError?: boolean): void;
 }
 
+/**
+ * @deprecated Eliminate the "host" concept. https://github.com/neovim/neovim/issues/27949
+ */
 export class Host {
   public loaded: { [index: string]: NvimPlugin };
 
diff --git a/packages/neovim/src/index.ts b/packages/neovim/src/index.ts
index 409a03ca..eea39f7d 100644
--- a/packages/neovim/src/index.ts
+++ b/packages/neovim/src/index.ts
@@ -1,4 +1,5 @@
 export { attach } from './attach/attach';
+export { cli } from './cli';
 export { Neovim, NeovimClient, Buffer, Tabpage, Window } from './api/index';
 export { Plugin, Function, Autocmd, Command } from './plugin';
 export { NvimPlugin } from './host/NvimPlugin';
diff --git a/packages/neovim/src/utils/transport.ts b/packages/neovim/src/utils/transport.ts
index 44260813..30a387a4 100644
--- a/packages/neovim/src/utils/transport.ts
+++ b/packages/neovim/src/utils/transport.ts
@@ -21,7 +21,7 @@ if (process.env.NODE_ENV === 'test') {
   };
 }
 
-class Response {
+export class Response {
   private requestId: number;
 
   private sent!: boolean;