diff --git a/changelogs/unreleased/gh-8147-tuple-format-in-iproto-responses.md b/changelogs/unreleased/gh-8147-tuple-format-in-iproto-responses.md
new file mode 100644
index 0000000000000000000000000000000000000000..9ec88ce079c1083f6bca4339b9734516724fb0ed
--- /dev/null
+++ b/changelogs/unreleased/gh-8147-tuple-format-in-iproto-responses.md
@@ -0,0 +1,5 @@
+## feature/box
+
+* Added support for sending tuple formats in IPROTO responses. Added a
+  `box_tuple_extension` backward compatibility option to disable sending
+  tuple formats in responses to IPROTO call and eval requests (gh-8146).
diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 84418fc782c52f12712055fa2aeb2e91f0f7b676..07772d9b5affd0a854ce9ff36297fbe5f0ba02b8 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -260,6 +260,7 @@ set(box_sources
     watcher.c
     decimal.c
     read_view.c
+    mp_box_ctx.c
     ${sql_sources}
     ${lua_sources}
     lua/init.c
diff --git a/src/box/iproto.cc b/src/box/iproto.cc
index ebed68eba13294fddaa440857c8b1c7efed0dea6..b5dc2ffbe2246b35b8e720bb7370bfae90d663b8 100644
--- a/src/box/iproto.cc
+++ b/src/box/iproto.cc
@@ -69,12 +69,14 @@
 #include "tt_static.h"
 #include "trivia/util.h"
 #include "salad/stailq.h"
-#include "assoc.h"
 #include "txn.h"
 #include "on_shutdown.h"
 #include "flightrec.h"
 #include "security.h"
 #include "watcher.h"
+#include "box/mp_box_ctx.h"
+#include "box/tuple.h"
+#include "mpstream/mpstream.h"
 
 enum {
 	IPROTO_SALT_SIZE = 32,
@@ -2172,6 +2174,14 @@ static void
 tx_process1(struct cmsg *m)
 {
 	struct iproto_msg *msg = tx_accept_msg(m);
+	bool box_tuple_as_ext =
+		iproto_features_test(&msg->connection->session->meta.features,
+				     IPROTO_FEATURE_DML_TUPLE_EXTENSION);
+	struct tuple_format_map format_map;
+	tuple_format_map_create_empty(&format_map);
+	auto format_map_guard = make_scoped_guard([&format_map] {
+		tuple_format_map_destroy(&format_map);
+	});
 	if (tx_check_msg(msg) != 0)
 		goto error;
 
@@ -2185,10 +2195,25 @@ tx_process1(struct cmsg *m)
 		goto error;
 	out = msg->connection->tx.p_obuf;
 	iproto_prepare_select(out, &svp);
-	if (tuple && tuple_to_obuf(tuple, out))
+	if (tuple != NULL) {
+		if (box_tuple_as_ext) {
+			tuple_format_map_add_format(&format_map,
+						    tuple->format_id);
+			if (tuple_to_obuf_as_ext(tuple, out) != 0)
+				goto error;
+		} else if (tuple_to_obuf(tuple, out) != 0) {
+			goto error;
+		}
+	}
+	/*
+	 * Even if there is no tuple, we still need to send an empty tuple
+	 * format map.
+	 */
+	if (box_tuple_as_ext &&
+	    tuple_format_map_to_iproto_obuf(&format_map, out) != 0)
 		goto error;
 	iproto_reply_select(out, &svp, msg->header.sync, ::schema_version,
-			    tuple != 0);
+			    tuple != 0, box_tuple_as_ext);
 	iproto_wpos_create(&msg->wpos, out);
 	tx_end_msg(msg, &svp);
 	return;
@@ -2203,9 +2228,24 @@ static void
 tx_process_select(struct cmsg *m)
 {
 	struct iproto_msg *msg = tx_accept_msg(m);
+	bool box_tuple_as_ext =
+		iproto_features_test(&msg->connection->session->meta.features,
+				     IPROTO_FEATURE_DML_TUPLE_EXTENSION);
 	struct obuf *out;
 	struct obuf_svp svp;
 	struct port port;
+
+	struct mp_box_ctx ctx;
+	struct mp_ctx *ctx_ref = NULL;
+	if (box_tuple_as_ext) {
+		mp_box_ctx_create(&ctx, NULL, NULL);
+		ctx_ref = (struct mp_ctx *)&ctx;
+	}
+	auto ctx_guard = make_scoped_guard([ctx_ref] {
+		mp_ctx_destroy(ctx_ref);
+	});
+	ctx_guard.is_active = box_tuple_as_ext;
+
 	int count;
 	int rc;
 	const char *packed_pos, *packed_pos_end;
@@ -2246,19 +2286,22 @@ tx_process_select(struct cmsg *m)
 	/*
 	 * SELECT output format has not changed since Tarantool 1.6
 	 */
-	count = port_dump_msgpack_16(&port, out);
+	count = port_dump_msgpack_16_with_ctx(&port, out, ctx_ref);
 	port_destroy(&port);
-	if (count < 0) {
+	if (count < 0 || (box_tuple_as_ext &&
+			  tuple_format_map_to_iproto_obuf(&ctx.tuple_format_map,
+							  out) != 0)) {
 		goto discard;
 	}
 	if (reply_position) {
 		assert(packed_pos != NULL);
 		iproto_reply_select_with_position(out, &svp, msg->header.sync,
 						  ::schema_version, count,
-						  packed_pos, packed_pos_end);
+						  packed_pos, packed_pos_end,
+						  box_tuple_as_ext);
 	} else {
 		iproto_reply_select(out, &svp, msg->header.sync,
-				    ::schema_version, count);
+				    ::schema_version, count, box_tuple_as_ext);
 	}
 	region_truncate(&fiber()->gc, region_svp);
 	iproto_wpos_create(&msg->wpos, out);
@@ -2290,6 +2333,21 @@ static void
 tx_process_call(struct cmsg *m)
 {
 	struct iproto_msg *msg = tx_accept_msg(m);
+
+	bool box_tuple_as_ext =
+		iproto_features_test(&msg->connection->session->meta.features,
+				     IPROTO_FEATURE_CALL_RET_TUPLE_EXTENSION);
+	struct mp_box_ctx ctx;
+	struct mp_ctx *ctx_ref = NULL;
+	if (box_tuple_as_ext) {
+		mp_box_ctx_create(&ctx, NULL, NULL);
+		ctx_ref = (struct mp_ctx *)&ctx;
+	}
+	auto ctx_guard = make_scoped_guard([ctx_ref] {
+		mp_ctx_destroy(ctx_ref);
+	});
+	ctx_guard.is_active = box_tuple_as_ext;
+
 	if (tx_check_msg(msg) != 0)
 		goto error;
 
@@ -2351,17 +2409,19 @@ tx_process_call(struct cmsg *m)
 	iproto_prepare_select(out, &svp);
 
 	if (msg->header.type == IPROTO_CALL_16)
-		count = port_dump_msgpack_16(&port, out);
+		count = port_dump_msgpack_16_with_ctx(&port, out, ctx_ref);
 	else
-		count = port_dump_msgpack(&port, out);
+		count = port_dump_msgpack_with_ctx(&port, out, ctx_ref);
+
 	port_destroy(&port);
-	if (count < 0) {
+	if (count < 0 || (box_tuple_as_ext &&
+			  tuple_format_map_to_iproto_obuf(&ctx.tuple_format_map,
+							  out) != 0)) {
 		obuf_rollback_to_svp(out, &svp);
 		goto error;
 	}
-
 	iproto_reply_select(out, &svp, msg->header.sync,
-			    ::schema_version, count);
+			    ::schema_version, count, box_tuple_as_ext);
 	iproto_wpos_create(&msg->wpos, out);
 	tx_end_msg(msg, &svp);
 	return;
@@ -2375,7 +2435,11 @@ tx_process_call(struct cmsg *m)
 static void
 tx_process_id(struct iproto_connection *con, const struct id_request *id)
 {
+	extern bool box_tuple_extension;
 	con->session->meta.features = id->features;
+	if (!box_tuple_extension)
+		iproto_features_clear(&con->session->meta.features,
+				      IPROTO_FEATURE_CALL_RET_TUPLE_EXTENSION);
 }
 
 /** Callback passed to session_watch. */
@@ -2439,7 +2503,8 @@ tx_process_misc(struct cmsg *m)
 		iproto_prepare_select(out, &header);
 		xobuf_dup(out, data, data_end - data);
 		iproto_reply_select(out, &header, msg->header.sync,
-				    ::schema_version, data != NULL ? 1 : 0);
+				    ::schema_version, data != NULL ? 1 : 0,
+				    /*box_tuple_as_ext=*/false);
 		break;
 	}
 	default:
diff --git a/src/box/iproto_constants.h b/src/box/iproto_constants.h
index ec18f273f48b805d11384008120ae55eaae56977..af7f432c77cfde2722d5d7ab24c3f258ac7c7e15 100644
--- a/src/box/iproto_constants.h
+++ b/src/box/iproto_constants.h
@@ -208,6 +208,11 @@ extern const char *iproto_flag_bit_strs[];
 	 * when identifier is present (i.e., the identifier is ignored).
 	 */								\
 	_(INDEX_NAME, 0x5f, MP_STR)					\
+	/**
+	 * Mapping of format identifier to format clause consisting of field
+	 * names and field types.
+	 */								\
+	_(TUPLE_FORMATS, 0x60, MP_MAP)
 
 #define IPROTO_KEY_MEMBER(s, v, ...) IPROTO_ ## s = v,
 
diff --git a/src/box/iproto_features.c b/src/box/iproto_features.c
index 1f2ffbde18599f6530a0039ae146cf0be6a65cdc..4aced8bb5c2a903805be00d42e24bdc0e9c13075 100644
--- a/src/box/iproto_features.c
+++ b/src/box/iproto_features.c
@@ -79,4 +79,8 @@ iproto_features_init(void)
 			    IPROTO_FEATURE_SPACE_AND_INDEX_NAMES);
 	iproto_features_set(&IPROTO_CURRENT_FEATURES,
 			    IPROTO_FEATURE_WATCH_ONCE);
+	iproto_features_set(&IPROTO_CURRENT_FEATURES,
+			    IPROTO_FEATURE_DML_TUPLE_EXTENSION);
+	iproto_features_set(&IPROTO_CURRENT_FEATURES,
+			    IPROTO_FEATURE_CALL_RET_TUPLE_EXTENSION);
 }
diff --git a/src/box/iproto_features.h b/src/box/iproto_features.h
index 2f1e2f15de2790ed9a188c7545d0ab9679241532..1d50a94ab10c30d9bc33dccc39cce170547ae76c 100644
--- a/src/box/iproto_features.h
+++ b/src/box/iproto_features.h
@@ -59,6 +59,18 @@ extern "C" {
 	_(SPACE_AND_INDEX_NAMES,  5)					\
 	/** IPROTO_WATCH_ONCE request support. */			\
 	_(WATCH_ONCE,  6)						\
+	/**
+	 * Tuple format in DML request responses support:
+	 * Tuples in IPROTO_DATA response field are encoded as MP_TUPLE and
+	 * tuple format is sent in IPROTO_TUPLE_FORMATS field.
+	 */								\
+	_(DML_TUPLE_EXTENSION, 7)					\
+	/**
+	 * Tuple format in call and eval request responses support:
+	 * Tuples in IPROTO_DATA response field are encoded as MP_TUPLE and
+	 * tuple formats are sent in IPROTO_TUPLE_FORMATS field.
+	 */								\
+	_(CALL_RET_TUPLE_EXTENSION, 8)					\
 
 #define IPROTO_FEATURE_MEMBER(s, v) IPROTO_FEATURE_ ## s = v,
 
diff --git a/src/box/lua/init.c b/src/box/lua/init.c
index 81bbf62b946868e3ba0cd783808bf1a804492b43..a476e8e39527cfd4a654fe55fef1fb1857841fd6 100644
--- a/src/box/lua/init.c
+++ b/src/box/lua/init.c
@@ -45,6 +45,9 @@
 #include "box/txn.h"
 #include "box/func.h"
 #include "box/mp_error.h"
+#include "box/mp_box_ctx.h"
+#include "box/mp_tuple.h"
+#include "box/tuple.h"
 
 #include "box/lua/error.h"
 #include "box/lua/tuple.h"
@@ -711,14 +714,21 @@ static const struct luaL_Reg boxlib_backup[] = {
  */
 static bool
 luamp_encode_extension_box(struct lua_State *L, int idx,
-			   struct mpstream *stream, struct mp_ctx *ctx,
+			   struct mpstream *stream, struct mp_ctx *base,
 			   enum mp_type *type)
 {
-	(void)ctx;
 	struct tuple *tuple = luaT_istuple(L, idx);
 	if (tuple != NULL) {
-		tuple_to_mpstream(tuple, stream);
-		*type = MP_ARRAY;
+		if (base != NULL) {
+			struct mp_box_ctx *ctx = mp_box_ctx_check(base);
+			tuple_to_mpstream_as_ext(tuple, stream);
+			tuple_format_map_add_format(&ctx->tuple_format_map,
+						    tuple->format_id);
+			*type = MP_EXT;
+		} else {
+			tuple_to_mpstream(tuple, stream);
+			*type = MP_ARRAY;
+		}
 		return true;
 	}
 	struct error *err = luaL_iserror(L, idx);
@@ -731,31 +741,44 @@ luamp_encode_extension_box(struct lua_State *L, int idx,
 }
 
 /**
- * A MsgPack extensions handler that supports errors decode.
+ * A MsgPack extensions handler that supports box extensions decode.
  */
 static void
 luamp_decode_extension_box(struct lua_State *L, const char **data,
 			   struct mp_ctx *ctx)
 {
-	(void)ctx;
 	assert(mp_typeof(**data) == MP_EXT);
 	int8_t ext_type;
 	uint32_t len = mp_decode_extl(data, &ext_type);
-
-	if (ext_type != MP_ERROR) {
+	switch (ext_type) {
+	case MP_ERROR: {
+		struct error *err = error_unpack(data, len);
+		if (err == NULL)
+			luaL_error(L, "Can not parse an error from MsgPack");
+		luaT_pusherror(L, err);
+		break;
+	}
+	case MP_TUPLE: {
+		struct tuple *tuple;
+		if (ctx == NULL) {
+			tuple = tuple_unpack_without_format(data);
+		} else {
+			struct tuple_format_map *tuple_format_map =
+				&mp_box_ctx_check(ctx)->tuple_format_map;
+			tuple = tuple_unpack(data, tuple_format_map);
+		}
+		if (tuple == NULL)
+			goto tuple_err;
+		luaT_pushtuple(L, tuple);
+		break;
+tuple_err:
+		luaL_error(L, "Can not parse a tuple from MsgPack");
+		break;
+	}
+	default:
 		luaL_error(L, "Unsupported MsgPack extension type: %d",
 			   ext_type);
-		return;
 	}
-
-	struct error *err = error_unpack(data, len);
-	if (err == NULL) {
-		luaL_error(L, "Can not parse an error from MsgPack");
-		return;
-	}
-
-	luaT_pusherror(L, err);
-	return;
 }
 
 #include "say.h"
diff --git a/src/box/lua/net_box.c b/src/box/lua/net_box.c
index 1ca4241519dc6d198f6c4d289c85b906da3e5ea0..faf2dbb69309012466d0f74ace3cff0ef8842e8b 100644
--- a/src/box/lua/net_box.c
+++ b/src/box/lua/net_box.c
@@ -49,8 +49,9 @@
 #include "box/execute.h"
 #include "box/error.h"
 #include "box/schema_def.h"
+#include "box/mp_box_ctx.h"
+#include "box/mp_tuple.h"
 
-#include "assoc.h"
 #include "coio.h"
 #include "fiber.h"
 #include "fiber_cond.h"
@@ -687,20 +688,22 @@ netbox_encode_ping(lua_State *L, int idx, struct netbox_method_encode_ctx *ctx)
  * Raises a Lua error on memory allocation failure.
  */
 static void
-netbox_encode_id(struct lua_State *L, struct ibuf *ibuf, uint64_t sync)
+netbox_encode_id(struct lua_State *L, struct ibuf *ibuf, uint64_t sync,
+		 bool fetch_schema)
 {
-	struct iproto_features *features = &NETBOX_IPROTO_FEATURES;
+	struct iproto_features features = NETBOX_IPROTO_FEATURES;
+	if (fetch_schema) {
+		iproto_features_clear(&features,
+				      IPROTO_FEATURE_DML_TUPLE_EXTENSION);
+	}
 #ifndef NDEBUG
-	struct iproto_features features_value;
 	struct errinj *errinj = errinj(ERRINJ_NETBOX_FLIP_FEATURE, ERRINJ_INT);
 	if (errinj->iparam >= 0 && errinj->iparam < iproto_feature_id_MAX) {
 		int feature_id = errinj->iparam;
-		features_value = *features;
-		features = &features_value;
-		if (iproto_features_test(features, feature_id))
-			iproto_features_clear(features, feature_id);
+		if (iproto_features_test(&features, feature_id))
+			iproto_features_clear(&features, feature_id);
 		else
-			iproto_features_set(features, feature_id);
+			iproto_features_set(&features, feature_id);
 	}
 #endif
 	struct mpstream stream;
@@ -712,9 +715,9 @@ netbox_encode_id(struct lua_State *L, struct ibuf *ibuf, uint64_t sync)
 	mpstream_encode_uint(&stream, IPROTO_VERSION);
 	mpstream_encode_uint(&stream, NETBOX_IPROTO_VERSION);
 	mpstream_encode_uint(&stream, IPROTO_FEATURES);
-	size_t size = mp_sizeof_iproto_features(features);
+	size_t size = mp_sizeof_iproto_features(&features);
 	char *data = mpstream_reserve(&stream, size);
-	mp_encode_iproto_features(data, features);
+	mp_encode_iproto_features(data, &features);
 	mpstream_advance(&stream, size);
 
 	netbox_end_encode(&stream, svp);
@@ -1492,6 +1495,10 @@ struct response_body {
 	const char *pos;
 	/* IPROTO_POSITION length */
 	uint32_t pos_len;
+	/* IPROTO_TUPLE_FORMATS */
+	const char *tuple_formats;
+	/* IPROTO_TUPLE_FORMATS end. */
+	const char *tuple_formats_end;
 };
 
 /*
@@ -1522,6 +1529,11 @@ response_body_decode(struct response_body *response_body, const char **data,
 				mp_decode_str(&value, &response_body->pos_len);
 			assert(response_body->pos_len != 0);
 			break;
+		case IPROTO_TUPLE_FORMATS:
+			assert(mp_typeof(*value) == MP_MAP);
+			response_body->tuple_formats = value;
+			response_body->tuple_formats_end = *data;
+			break;
 		default:
 			break;
 		}
@@ -1561,11 +1573,17 @@ netbox_decode_table(struct lua_State *L, const char **data,
 		lua_pushnil(L);
 		return;
 	}
+	struct mp_box_ctx ctx;
+	mp_box_ctx_create(&ctx, NULL, response_body.tuple_formats);
 	if (return_raw) {
-		luamp_push(L, response_body.data, response_body.data_end);
+		luamp_push_with_ctx(L, response_body.data,
+				    response_body.data_end,
+				    (struct mp_ctx *)&ctx);
 	} else {
-		luamp_decode(L, cfg, &response_body.data);
+		luamp_decode_with_ctx(L, cfg, &response_body.data,
+				      (struct mp_ctx *)&ctx);
 	}
+	mp_ctx_destroy((struct mp_ctx *)&ctx);
 }
 
 /**
@@ -1585,11 +1603,17 @@ netbox_decode_value(struct lua_State *L, const char **data,
 		lua_pushnil(L);
 		return;
 	}
+	struct mp_box_ctx ctx;
+		mp_box_ctx_create(&ctx, NULL, response_body.tuple_formats);
 	if (return_raw) {
-		luamp_push(L, response_body.data, response_body.data_end);
+		luamp_push_with_ctx(L, response_body.data,
+				    response_body.data_end,
+				    (struct mp_ctx *)&ctx);
 	} else {
-		luamp_decode(L, cfg, &response_body.data);
+		luamp_decode_with_ctx(L, cfg, &response_body.data,
+				      (struct mp_ctx *)&ctx);
 	}
+	mp_ctx_destroy((struct mp_ctx *)&ctx);
 }
 
 /**
@@ -1610,17 +1634,22 @@ netbox_decode_count(struct lua_State *L, const char **data,
  */
 static void
 netbox_decode_data(struct lua_State *L, const char **data,
-		   struct tuple_format *format)
+		   struct tuple_format *format, struct mp_box_ctx *ctx)
 {
 	uint32_t count = mp_decode_array(data);
 	lua_createtable(L, count, 0);
 	for (uint32_t j = 0; j < count; ++j) {
 		const char *begin = *data;
 		mp_next(data);
-		struct tuple *tuple =
-			box_tuple_new(format, begin, *data);
-		if (tuple == NULL)
+		struct tuple *tuple;
+		if (tuple_format_map_is_empty(&ctx->tuple_format_map))
+			tuple = box_tuple_new(format, begin, *data);
+		else
+			tuple = mp_decode_tuple(&begin, &ctx->tuple_format_map);
+		if (tuple == NULL) {
+			mp_ctx_destroy((struct mp_ctx *)ctx);
 			luaT_error(L);
+		}
 		luaT_pushtuple(L, tuple);
 		lua_rawseti(L, -2, j + 1);
 	}
@@ -1637,11 +1666,16 @@ netbox_decode_select(struct lua_State *L, const char **data,
 {
 	struct response_body response_body;
 	response_body_decode(&response_body, data, data_end);
+	struct mp_box_ctx ctx;
+	mp_box_ctx_create(&ctx, NULL, response_body.tuple_formats);
 	if (return_raw) {
-		luamp_push(L, response_body.data, response_body.data_end);
+		luamp_push_with_ctx(L, response_body.data,
+				    response_body.data_end,
+				    (struct mp_ctx *)&ctx);
 	} else {
-		netbox_decode_data(L, &response_body.data, format);
+		netbox_decode_data(L, &response_body.data, format, &ctx);
 	}
+	mp_ctx_destroy((struct mp_ctx *)&ctx);
 }
 
 /**
@@ -1658,11 +1692,18 @@ netbox_decode_select_with_pos(struct lua_State *L, const char **data,
 	response_body_decode(&response_body, data, data_end);
 	lua_createtable(L, response_body.pos != NULL ? 2 : 1, 0);
 	int table_idx = lua_gettop(L);
+	struct mp_box_ctx ctx;
+	mp_box_ctx_create(&ctx, NULL, response_body.tuple_formats);
 	if (return_raw) {
-		luamp_push(L, response_body.data, response_body.data_end);
+		luamp_push_with_ctx(L, response_body.data,
+				    response_body.data_end,
+				    (struct mp_ctx *)&ctx);
 	} else {
-		netbox_decode_data(L, &response_body.data, format);
+		struct mp_box_ctx ctx;
+		mp_box_ctx_create(&ctx, NULL, response_body.tuple_formats);
+		netbox_decode_data(L, &response_body.data, format, &ctx);
 	}
+	mp_ctx_destroy((struct mp_ctx *)&ctx);
 	lua_rawseti(L, table_idx, 1);
 	if (response_body.pos != NULL) {
 		lua_pushlstring(L, response_body.pos, response_body.pos_len);
@@ -1687,13 +1728,29 @@ netbox_decode_tuple(struct lua_State *L, const char **data,
 		return;
 	}
 	if (return_raw) {
-		luamp_push(L, response_body.data, response_body.data_end);
+		struct mp_box_ctx ctx;
+		mp_box_ctx_create(&ctx, NULL, response_body.tuple_formats);
+		luamp_push_with_ctx(L, response_body.data,
+				    response_body.data_end,
+				    (struct mp_ctx *)&ctx);
 	} else {
-		struct tuple *tuple =
-			box_tuple_new(format, response_body.data,
-				      response_body.data_end);
-		if (tuple == NULL)
+		struct tuple *tuple;
+		if (response_body.tuple_formats == NULL) {
+			tuple = box_tuple_new(format, response_body.data,
+					      response_body.data_end);
+		} else {
+			struct tuple_format_map tuple_format_map;
+			const char *tuple_formats = response_body.tuple_formats;
+			if (tuple_format_map_create_from_mp(&tuple_format_map,
+							    tuple_formats) != 0)
+				luaT_error(L);
+			tuple = mp_decode_tuple(&response_body.data,
+						&tuple_format_map);
+			tuple_format_map_destroy(&tuple_format_map);
+		}
+		if (tuple == NULL) {
 			luaT_error(L);
+		}
 		luaT_pushtuple(L, tuple);
 	}
 }
@@ -1833,8 +1890,11 @@ netbox_decode_execute(struct lua_State *L, const char **data,
 				mp_next(data);
 				luamp_push(L, begin, *data);
 			} else {
+				struct mp_box_ctx ctx;
+				mp_box_ctx_create(&ctx, NULL, NULL);
 				netbox_decode_data(L, data,
-						   tuple_format_runtime);
+						   tuple_format_runtime, &ctx);
+				mp_ctx_destroy((struct mp_ctx *)&ctx);
 			}
 			rows_index = lua_gettop(L);
 			break;
@@ -2570,9 +2630,10 @@ netbox_transport_on_event(struct netbox_transport *transport,
 }
 
 /**
- * Argument data is the body of response, it must be an MP_MAP. Only two keys
- * are expected to appear: IPROTO_DATA (necessarily, must be the first one)
- * and IPROTO_POSITION (unnecessarily). The function writes response to
+ * Argument data is the body of response, it must be an MP_MAP. Only three keys
+ * are expected to appear: IPROTO_DATA (necessarily, must be the first one),
+ * IPROTO_TUPLE_FORMATS (optionally), and
+ * IPROTO_POSITION (optionally). The function writes response to
  * passed ibuf. If skip_header flag is set, data is written without IPROTO_DATA
  * header. If skip_header is true and response contains IPROTO_POSITION,
  * position is not written to a buffer - the function returns a table with
@@ -2588,40 +2649,23 @@ netbox_write_response_to_buffer(const char *data, const char *data_end,
 	/* Copy xrow.body to user-provided buffer. */
 	size_t data_len = data_end - data;
 	bool return_table = false;
+	struct response_body response_body;
+	const char *data_ptr = data;
+	response_body_decode(&response_body, &data_ptr, data_end);
 	if (skip_header) {
-		assert(mp_typeof(*data) == MP_MAP);
-		uint32_t map_size = mp_decode_map(&data);
-		uint32_t key = mp_decode_uint(&data);
-		assert(key == IPROTO_DATA);
-		(void)key;
-		if (map_size > 1) {
-			/*
-			 * The map has more than one element iff it
-			 * contains IPROTO_DATA and IPROTO_POSITION
-			 * and they are placed exactly in this order.
-			 */
-			assert(map_size == 2);
-			/* Find the end of IPROTO_DATA and its len. */
-			const char *iproto_position = data;
-			mp_next(&iproto_position);
-			data_len = iproto_position - data;
+		data = response_body.data;
+		data_len = response_body.data_end - response_body.data;
+		if (response_body.pos != NULL) {
 			/* Create table to return 2 values. */
 			return_table = true;
 			lua_createtable(L, 2, 0);
-			/* Skip IPROTO_POSITION key. */
-			key = mp_decode_uint(&iproto_position);
-			assert(key == IPROTO_POSITION);
 			/* Check position length */
-			assert(mp_typeof(*iproto_position) == MP_STR);
-			uint32_t str_len = mp_decode_strl(&iproto_position);
-			if (str_len != 0) {
+			if (response_body.pos_len != 0) {
 				/* Set position to the 2nd place in table. */
-				lua_pushlstring(L, iproto_position, str_len);
+				lua_pushlstring(L, response_body.pos,
+						response_body.pos_len);
 				lua_rawseti(L, -2, 2);
 			}
-		} else {
-			/* Update data_len if header is skipped. */
-			data_len = data_end - data;
 		}
 	}
 	void *wpos = ibuf_alloc(buffer, data_len);
@@ -2733,7 +2777,8 @@ netbox_transport_do_id(struct netbox_transport *transport, struct lua_State *L)
 	ERROR_INJECT(ERRINJ_NETBOX_DISABLE_ID, goto out);
 	if (peer_version_id < version_id(2, 10, 0))
 		goto unsupported;
-	netbox_encode_id(L, &transport->send_buf, transport->next_sync++);
+	netbox_encode_id(L, &transport->send_buf, transport->next_sync++,
+			 transport->opts.fetch_schema);
 	struct xrow_header hdr;
 	if (netbox_transport_send_and_recv(transport, &hdr) != 0)
 		luaT_error(L);
@@ -3163,6 +3208,10 @@ luaopen_net_box(struct lua_State *L)
 			    IPROTO_FEATURE_SPACE_AND_INDEX_NAMES);
 	iproto_features_set(&NETBOX_IPROTO_FEATURES,
 			    IPROTO_FEATURE_WATCH_ONCE);
+	iproto_features_set(&NETBOX_IPROTO_FEATURES,
+			    IPROTO_FEATURE_DML_TUPLE_EXTENSION);
+	iproto_features_set(&NETBOX_IPROTO_FEATURES,
+			    IPROTO_FEATURE_CALL_RET_TUPLE_EXTENSION);
 
 	lua_pushcfunction(L, luaT_netbox_request_iterator_next);
 	luaT_netbox_request_iterator_next_ref = luaL_ref(L, LUA_REGISTRYINDEX);
diff --git a/src/box/lua/tuple.c b/src/box/lua/tuple.c
index ab11d4cdf44f3456aa7e182050fdf2ac329b8557..1fed5050603e696114efa4e331832cf142644671 100644
--- a/src/box/lua/tuple.c
+++ b/src/box/lua/tuple.c
@@ -441,6 +441,41 @@ luamp_encode_tuple_with_ctx(struct lua_State *L, struct luaL_serializer *cfg,
 		return 0;
 	}
 
+	/*
+	 * This snippet handles a special when a box tuple is sent over IPROTO
+	 * as MP_TUPLE and is decoded by net.box using the `return_raw` option,
+	 * which return a MsgPack object. This case is semantically equivalent
+	 * to the case above where the MP_TUPLE should have been decoded as a
+	 * box tuple. While we expect the encoded MsgPack to be an MP_ARRAY, the
+	 * Lua MsgPack encoder below encodes MsgPack objects by simply copying
+	 * their contents to the MsgPack stream, so we would get MP_TUPLE as the
+	 * returned type.
+	 *
+	 * To overcome this limitation, we convert the top-level MP_TUPLE to a
+	 * MsgPack array by skipping the extension header and format identifier
+	 * and copying the tuple data to the MsgPack stream.
+	 */
+	size_t data_len;
+	const char *data = luamp_get(L, index, &data_len);
+	if (data != NULL) {
+		if (mp_typeof(*data) == MP_EXT) {
+			const char *tuple_data = data;
+			int8_t ext_type;
+			uint32_t tuple_data_len =
+				mp_decode_extl(&tuple_data, &ext_type);
+			if (ext_type == MP_TUPLE) {
+				/* Skip the tuple format identifier. */
+				assert(mp_typeof(*tuple_data) == MP_UINT);
+				uint64_t format_id =
+					mp_decode_uint(&tuple_data);
+				tuple_data_len -= mp_sizeof_uint(format_id);
+				mpstream_memcpy(stream, tuple_data,
+						tuple_data_len);
+				return 0;
+			}
+		}
+	}
+
 	enum mp_type type;
 	if (luamp_encode_with_ctx(L, cfg, stream, index, ctx, &type) != 0)
 		return -1;
diff --git a/src/box/mp_box_ctx.c b/src/box/mp_box_ctx.c
new file mode 100644
index 0000000000000000000000000000000000000000..d53d337679c35a4b23f2eb2db6b86c253a64b6e1
--- /dev/null
+++ b/src/box/mp_box_ctx.c
@@ -0,0 +1,21 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2023, Tarantool AUTHORS, please see AUTHORS file.
+ */
+
+#include "box/mp_box_ctx.h"
+
+void
+mp_box_ctx_destroy(struct mp_ctx *ctx)
+{
+	tuple_format_map_destroy(&mp_box_ctx_check(ctx)->tuple_format_map);
+}
+
+void
+mp_box_ctx_move(struct mp_ctx *dst, struct mp_ctx *src)
+{
+	mp_ctx_move_default(dst, src);
+	tuple_format_map_move(&mp_box_ctx_check(dst)->tuple_format_map,
+			      &mp_box_ctx_check(src)->tuple_format_map);
+}
diff --git a/src/box/mp_box_ctx.h b/src/box/mp_box_ctx.h
new file mode 100644
index 0000000000000000000000000000000000000000..fd76ec23d4c3ed6cc0b923a18a4ab0508ca792af
--- /dev/null
+++ b/src/box/mp_box_ctx.h
@@ -0,0 +1,72 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2023, Tarantool AUTHORS, please see AUTHORS file.
+ */
+
+#pragma once
+
+#include "core/mp_ctx.h"
+
+#include "box/tuple_format_map.h"
+
+#include <assert.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+/**
+ * Context for MsgPack encoding/decoding of box-specific types.
+ */
+struct mp_box_ctx {
+	/** See `mp_ctx::translation`. */
+	struct mh_strnu32_t *translation;
+	/** See `mp_ctx::destroy`. */
+	void (*destroy)(struct mp_ctx *ctx);
+	/** See `mp_ctx::move`. */
+	void (*move)(struct mp_ctx *dst, struct mp_ctx *src);
+	/** Mapping of format identifiers to tuple formats. */
+	struct tuple_format_map tuple_format_map;
+};
+
+static_assert(sizeof(struct mp_box_ctx) <= sizeof(struct mp_ctx),
+	      "sizeof(struct mp_box_ctx) must be <= sizeof(struct mp_ctx)");
+
+/**
+ * 'Virtual' destructor. Must not be called directly.
+ */
+void
+mp_box_ctx_destroy(struct mp_ctx *ctx);
+
+/**
+ * 'Virtual' move. Must not be called directly.
+ */
+void
+mp_box_ctx_move(struct mp_ctx *dst, struct mp_ctx *src);
+
+static inline struct mp_box_ctx *
+mp_box_ctx_check(struct mp_ctx *base)
+{
+	assert(base->destroy == mp_box_ctx_destroy &&
+	       base->move == mp_box_ctx_move);
+	return (struct mp_box_ctx *)base;
+}
+
+static inline int
+mp_box_ctx_create(struct mp_box_ctx *ctx, struct mh_strnu32_t *translation,
+		  const char *tuple_formats)
+{
+	mp_ctx_create((struct mp_ctx *)ctx, translation, mp_box_ctx_destroy,
+		      mp_box_ctx_move);
+	if (tuple_formats == NULL) {
+		tuple_format_map_create_empty(&ctx->tuple_format_map);
+		return 0;
+	}
+	return tuple_format_map_create_from_mp(&ctx->tuple_format_map,
+					       tuple_formats);
+}
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* defined(__cplusplus) */
diff --git a/src/box/port.c b/src/box/port.c
index e8c23a0711ed05b03927faa15999a4a1409b1840..c77d3a1a90f52621311ff76649ca24e50dc04f7d 100644
--- a/src/box/port.c
+++ b/src/box/port.c
@@ -38,6 +38,7 @@
 #include "errinj.h"
 #include "mpstream/mpstream.h"
 #include "tweaks.h"
+#include "box/mp_box_ctx.h"
 
 /**
  * The pool is used by port_c to allocate entries and to store
@@ -207,14 +208,22 @@ port_c_add_str(struct port *base, const char *str, uint32_t len)
 static int
 port_c_dump_msgpack(struct port *base, struct obuf *out, struct mp_ctx *ctx)
 {
-	(void)ctx;
 	struct port_c *port = (struct port_c *)base;
 	struct port_c_entry *pe;
 	for (pe = port->first; pe != NULL; pe = pe->next) {
 		uint32_t size = pe->mp_size;
 		if (size == 0) {
-			if (tuple_to_obuf(pe->tuple, out) != 0)
+			if (ctx != NULL) {
+				if (tuple_to_obuf_as_ext(pe->tuple, out) != 0)
+					return -1;
+				struct mp_box_ctx *box_ctx =
+					mp_box_ctx_check(ctx);
+				tuple_format_map_add_format(
+					&box_ctx->tuple_format_map,
+					pe->tuple->format_id);
+			} else if (tuple_to_obuf(pe->tuple, out) != 0) {
 				return -1;
+			}
 		} else if (obuf_dup(out, pe->mp, size) != size) {
 			diag_set(OutOfMemory, size, "obuf_dup", "data");
 			return -1;
diff --git a/src/box/tuple_format_map.c b/src/box/tuple_format_map.c
index a15ac0bd632c49c0f0d0212d5e07f05395659a7e..bc50c2b6787ea86f3eaa5ceb4dc3ea95c2548c67 100644
--- a/src/box/tuple_format_map.c
+++ b/src/box/tuple_format_map.c
@@ -4,6 +4,7 @@
  * Copyright 2010-2023, Tarantool AUTHORS, please see AUTHORS file.
  */
 
+#include "box/iproto_constants.h"
 #include "box/tuple_format_map.h"
 #include "box/tuple.h"
 #include "diag.h"
@@ -135,6 +136,15 @@ tuple_format_map_destroy(struct tuple_format_map *map)
 	TRASH(map);
 }
 
+void
+tuple_format_map_move(struct tuple_format_map *dst,
+		      struct tuple_format_map *src)
+{
+	memcpy(dst, src, sizeof(*dst));
+	src->cache_last_index = -1;
+	src->hash_table = NULL;
+}
+
 void
 tuple_format_map_add_format(struct tuple_format_map *map, uint16_t format_id)
 {
@@ -160,6 +170,31 @@ tuple_format_map_to_mpstream(struct tuple_format_map *map,
 	}
 }
 
+static void
+mpstream_error(void *is_err)
+{
+	*(bool *)is_err = true;
+}
+
+int
+tuple_format_map_to_iproto_obuf(struct tuple_format_map *map,
+				struct obuf *obuf)
+{
+	struct mpstream stream;
+	bool is_error = false;
+	mpstream_init(&stream, obuf, obuf_reserve_cb, obuf_alloc_cb,
+		      mpstream_error, &is_error);
+	mpstream_encode_uint(&stream, IPROTO_TUPLE_FORMATS);
+	tuple_format_map_to_mpstream(map, &stream);
+	mpstream_flush(&stream);
+	if (is_error) {
+		diag_set(OutOfMemory, stream.pos - stream.buf,
+			 "mpstream_flush", "stream");
+		return -1;
+	}
+	return 0;
+}
+
 struct tuple_format *
 tuple_format_map_find(struct tuple_format_map *map, uint16_t format_id)
 {
diff --git a/src/box/tuple_format_map.h b/src/box/tuple_format_map.h
index 1acaabd8cc65d6be4068575acb75ab8d2226ed9c..53560292c0b865963937dfe25db4872beac6fabe 100644
--- a/src/box/tuple_format_map.h
+++ b/src/box/tuple_format_map.h
@@ -61,6 +61,14 @@ tuple_format_map_create_from_mp(struct tuple_format_map *map, const char *data);
 void
 tuple_format_map_destroy(struct tuple_format_map *map);
 
+/**
+ * Move the tuple format from @a src to @a dst and destroy @a src.
+ * The destination format map must be empty or uninitialized.
+ */
+void
+tuple_format_map_move(struct tuple_format_map *dst,
+		      struct tuple_format_map *src);
+
 static inline bool
 tuple_format_map_is_empty(struct tuple_format_map *map)
 {
@@ -80,6 +88,15 @@ void
 tuple_format_map_to_mpstream(struct tuple_format_map *map,
 			     struct mpstream *stream);
 
+/**
+ * Serialize a tuple format map as `IPROTO_TUPLE_FORMATS` to an output buffer.
+ *
+ * Returns 0 on success, otherwise -1 and sets diagnostic.
+ */
+int
+tuple_format_map_to_iproto_obuf(struct tuple_format_map *map,
+				struct obuf *obuf);
+
 /**
  * Find a format in the tuple format map.
  *
diff --git a/src/box/xrow.c b/src/box/xrow.c
index 18e5f4a63ddb27e0eefe7ddf7b750eaa59073d5d..f2babcecebf5a0f4f24dcbf0480c9420304047b6 100644
--- a/src/box/xrow.c
+++ b/src/box/xrow.c
@@ -46,6 +46,14 @@
 #include "iproto_features.h"
 #include "mpstream/mpstream.h"
 #include "errinj.h"
+#include "core/tweaks.h"
+
+/**
+ * Controls whether `IPROTO_FEATURE_CALL_RET_TUPLE_EXTENSION` feature bit is set
+ * in `IPROTO_ID` request responses.
+ */
+bool box_tuple_extension;
+TWEAK_BOOL(box_tuple_extension);
 
 /**
  * Min length of the salt sent in a greeting message.
@@ -483,23 +491,22 @@ iproto_reply_id(struct obuf *out, const char *auth_type,
 	assert(auth_type != NULL);
 	uint32_t auth_type_len = strlen(auth_type);
 	unsigned version = IPROTO_CURRENT_VERSION;
-	struct iproto_features *features = &IPROTO_CURRENT_FEATURES;
-
+	struct iproto_features features = IPROTO_CURRENT_FEATURES;
+	if (!box_tuple_extension)
+		iproto_features_clear(&features,
+				      IPROTO_FEATURE_CALL_RET_TUPLE_EXTENSION);
 #ifndef NDEBUG
 	struct errinj *errinj;
 	errinj = errinj(ERRINJ_IPROTO_SET_VERSION, ERRINJ_INT);
 	if (errinj->iparam >= 0)
 		version = errinj->iparam;
-	struct iproto_features features_value;
 	errinj = errinj(ERRINJ_IPROTO_FLIP_FEATURE, ERRINJ_INT);
 	if (errinj->iparam >= 0 && errinj->iparam < iproto_feature_id_MAX) {
 		int feature_id = errinj->iparam;
-		features_value = *features;
-		features = &features_value;
-		if (iproto_features_test(features, feature_id))
-			iproto_features_clear(features, feature_id);
+		if (iproto_features_test(&features, feature_id))
+			iproto_features_clear(&features, feature_id);
 		else
-			iproto_features_set(features, feature_id);
+			iproto_features_set(&features, feature_id);
 	}
 #endif
 
@@ -508,7 +515,7 @@ iproto_reply_id(struct obuf *out, const char *auth_type,
 	size += mp_sizeof_uint(IPROTO_VERSION);
 	size += mp_sizeof_uint(version);
 	size += mp_sizeof_uint(IPROTO_FEATURES);
-	size += mp_sizeof_iproto_features(features);
+	size += mp_sizeof_iproto_features(&features);
 	size += mp_sizeof_uint(IPROTO_AUTH_TYPE);
 	size += mp_sizeof_str(auth_type_len);
 
@@ -518,7 +525,7 @@ iproto_reply_id(struct obuf *out, const char *auth_type,
 	data = mp_encode_uint(data, IPROTO_VERSION);
 	data = mp_encode_uint(data, version);
 	data = mp_encode_uint(data, IPROTO_FEATURES);
-	data = mp_encode_iproto_features(data, features);
+	data = mp_encode_iproto_features(data, &features);
 	data = mp_encode_uint(data, IPROTO_AUTH_TYPE);
 	data = mp_encode_str(data, auth_type, auth_type_len);
 	assert(size == (size_t)(data - buf));
@@ -713,7 +720,8 @@ iproto_prepare_header(struct obuf *buf, struct obuf_svp *svp, size_t size)
 /** Reply select with IPROTO_DATA. */
 void
 iproto_reply_select(struct obuf *buf, struct obuf_svp *svp, uint64_t sync,
-		    uint64_t schema_version, uint32_t count)
+		    uint64_t schema_version, uint32_t count,
+		    bool box_tuple_as_ext)
 {
 	char *pos = (char *) obuf_svp_to_ptr(buf, svp);
 	iproto_header_encode(pos, IPROTO_OK, sync, schema_version,
@@ -721,6 +729,7 @@ iproto_reply_select(struct obuf *buf, struct obuf_svp *svp, uint64_t sync,
 			     IPROTO_HEADER_LEN);
 
 	struct iproto_body_bin body = iproto_body_bin;
+	body.m_body += box_tuple_as_ext;
 	body.v_data_len = mp_bswap_u32(count);
 
 	memcpy(pos + IPROTO_HEADER_LEN, &body, sizeof(body));
@@ -731,7 +740,8 @@ void
 iproto_reply_select_with_position(struct obuf *buf, struct obuf_svp *svp,
 				  uint64_t sync, uint32_t schema_version,
 				  uint32_t count, const char *packed_pos,
-				  const char *packed_pos_end)
+				  const char *packed_pos_end,
+				  bool box_tuple_as_ext)
 {
 	size_t packed_pos_size = packed_pos_end - packed_pos;
 	size_t key_size = mp_sizeof_uint(IPROTO_POSITION);
@@ -747,6 +757,7 @@ iproto_reply_select_with_position(struct obuf *buf, struct obuf_svp *svp,
 			     IPROTO_HEADER_LEN);
 
 	struct iproto_body_bin body = iproto_body_bin_with_position;
+	body.m_body += box_tuple_as_ext;
 	body.v_data_len = mp_bswap_u32(count);
 
 	memcpy(pos + IPROTO_HEADER_LEN, &body, sizeof(body));
diff --git a/src/box/xrow.h b/src/box/xrow.h
index 5fdcbbf15e709fb2c694d9ac5ef2efce87217343..683f83bf3102a76c888e018d2ce178e9f3345fb3 100644
--- a/src/box/xrow.h
+++ b/src/box/xrow.h
@@ -779,7 +779,8 @@ iproto_prepare_select_with_position(struct obuf *buf, struct obuf_svp *svp)
  */
 void
 iproto_reply_select(struct obuf *buf, struct obuf_svp *svp, uint64_t sync,
-		    uint64_t schema_version, uint32_t count);
+		    uint64_t schema_version, uint32_t count,
+		    bool box_tuple_as_ext);
 
 /**
  * Write extended select header to a preallocated buffer.
@@ -788,7 +789,8 @@ void
 iproto_reply_select_with_position(struct obuf *buf, struct obuf_svp *svp,
 				  uint64_t sync, uint32_t schema_version,
 				  uint32_t count, const char *packed_pos,
-				  const char *packed_pos_end);
+				  const char *packed_pos_end,
+				  bool box_tuple_as_ext);
 
 /**
  * Encode iproto header with IPROTO_OK response code.
diff --git a/src/lua/compat.lua b/src/lua/compat.lua
index 735f6569a7e4f064bb31744ebe3f383b6729bc01..b8429b64f7ceaed47c9f00577e631eb77f0f657e 100644
--- a/src/lua/compat.lua
+++ b/src/lua/compat.lua
@@ -96,6 +96,12 @@ additional msgpack array when returning them via iproto.
 https://tarantool.io/compat/c_func_iproto_multireturn
 ]]
 
+local BOX_TUPLE_EXTENSION_BRIEF = [[
+Controls IPROTO_FEATURE_CALL_RET_TUPLE_EXTENSION feature bit.
+
+https://tarantool.io/compat/box_tuple_extension
+]]
+
 -- Returns an action callback that toggles a tweak.
 local function tweak_action(tweak_name, old_tweak_value, new_tweak_value)
     return function(is_new)
@@ -182,6 +188,13 @@ local options = {
         run_action_now = true,
         action = tweak_action('c_func_iproto_multireturn', false, true),
     },
+    box_tuple_extension = {
+        default = 'new',
+        obsolete = nil,
+        brief = BOX_TUPLE_EXTENSION_BRIEF,
+        run_action_now = true,
+        action = tweak_action('box_tuple_extension', false, true)
+    },
 }
 
 -- Array with option names in order of addition.
diff --git a/test/box-luatest/gh_7894_export_iproto_constants_and_features_test.lua b/test/box-luatest/gh_7894_export_iproto_constants_and_features_test.lua
index 78fdb581d2ebe6615295472452b91a984370a3c8..1c9fb190789357ad757e95e34a204cc3483a62ef 100644
--- a/test/box-luatest/gh_7894_export_iproto_constants_and_features_test.lua
+++ b/test/box-luatest/gh_7894_export_iproto_constants_and_features_test.lua
@@ -84,6 +84,7 @@ local reference_table = {
         INSTANCE_NAME = 0x5d,
         SPACE_NAME = 0x5e,
         INDEX_NAME = 0x5f,
+        TUPLE_FORMATS = 0x60,
     },
 
     -- `iproto_metadata_key` enumeration.
@@ -175,6 +176,8 @@ local reference_table = {
         pagination = true,
         space_and_index_names = true,
         watch_once = true,
+        dml_tuple_extension = true,
+        call_ret_tuple_extension = true,
     },
     feature = {
         streams = 0,
@@ -184,6 +187,8 @@ local reference_table = {
         pagination = 4,
         space_and_index_names = 5,
         watch_once = 6,
+        dml_tuple_extension = 7,
+        call_ret_tuple_extension = 8,
     },
 }
 
diff --git a/test/box-luatest/gh_8147_tuple_formats_in_iproto_responses_test.lua b/test/box-luatest/gh_8147_tuple_formats_in_iproto_responses_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..4e250aeeb382f02d632e2a5b6c17e8c4d6471f97
--- /dev/null
+++ b/test/box-luatest/gh_8147_tuple_formats_in_iproto_responses_test.lua
@@ -0,0 +1,168 @@
+local buffer = require('buffer')
+local msgpack = require('msgpack')
+local netbox = require('net.box')
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group(nil, t.helpers.matrix{return_raw = {false, true}})
+
+g.before_all(function(cg)
+    cg.server = server:new()
+    cg.server:start()
+    cg.server:exec(function()
+        local s = box.schema.space.create('s')
+        s:create_index('i')
+        local sf = box.schema.space.create('sf', {format = {'field'}})
+        sf:create_index('i')
+        rawset(_G, 'f', function() return 1, 2, 3 end)
+        rawset(_G, 't', function() return {1, {t = s:get{0}}} end)
+        rawset(_G, 'tf', function() return {1, {t = sf:get{0}}} end)
+    end)
+end)
+
+g.after_all(function(cg)
+    cg.server:drop()
+end)
+
+local function res_wrapper(res, unpacked)
+    if msgpack.is_object(res) then
+        res = res:decode()
+        return unpacked and unpack(res) or res
+    end
+    return res
+end
+
+local function test_tuple_method(c, method, tuple, return_raw)
+    local res = res_wrapper(c.space.s[method](c.space.s, tuple,
+                                              {return_raw = return_raw}))
+    t.assert_equals(res:totable(), tuple:totable())
+    local resf = res_wrapper(c.space.sf[method](c.space.sf, tuple,
+                                                {return_raw = return_raw}))
+    t.assert_equals(resf:tomap{names_only = true},
+                    tuple:tomap{names_only = true})
+end
+
+local function get_call_eval_res(c, method, ret_raw)
+    if method == 'call' then
+        return res_wrapper(c:call("t", {}, {return_raw = ret_raw}), true)[2].t,
+                res_wrapper(c:call("tf", {}, {return_raw = ret_raw}), true)[2].t
+    else
+        return res_wrapper(c:eval("return t()", {},
+                                  {return_raw = ret_raw}), true)[2].t,
+               res_wrapper(c:eval("return tf()", {},
+                                  {return_raw = ret_raw}), true)[2].t
+    end
+end
+
+local function test_call_eval_box_tuple_extension(c, method, tuple, ret_raw)
+    local res, resf = get_call_eval_res(c, method, ret_raw)
+    t.assert(box.tuple.is(res))
+    t.assert_equals(res:totable(), tuple:totable())
+    t.assert(box.tuple.is(resf))
+    t.assert_equals(resf:tomap{names_only = true},
+                    tuple:tomap{names_only = true})
+end
+
+local function test_call_eval_old(c, method, tuple, ret_raw)
+    local res, resf = get_call_eval_res(c, method, ret_raw)
+    t.assert_not(box.tuple.is(res))
+    t.assert_equals(res, tuple:totable())
+    t.assert_not(box.tuple.is(resf))
+    t.assert_equals(resf, tuple:totable())
+end
+
+local function pack(...)
+    return { n = select("#", ...), ... }
+end
+
+-- Checks that formats in IPROTO request responses work correctly.
+g.test_net_box_formats_in_iproto_request_responses = function(cg)
+    local ret_raw = cg.params.return_raw
+    local c = netbox:connect(cg.server.net_box_uri, {fetch_schema = false})
+
+    t.assert_equals(c.space.s:get{0}, nil)
+    t.assert_equals(c.space.sf:get{0}, nil)
+    t.assert_equals(c.space.s:select{}, {})
+    t.assert_equals(c.space.sf:select{}, {})
+    local tuple_methods = {'insert', 'delete', 'replace', 'get'}
+    local fmt = box.tuple.format.new{{'field'}}
+    local tuple = box.tuple.new({0}, {format = fmt})
+    for _, method in ipairs(tuple_methods) do
+        test_tuple_method(c, method, tuple)
+    end
+    local res = res_wrapper(c.space.s:update({0}, {{'=', 2, 0}},
+                                             {return_raw = ret_raw}))
+    t.assert_equals(res:totable(), {0, 0})
+    c.space.s:replace{0}
+    local resf = res_wrapper(c.space.sf:update({0}, {{'=', 2, 0}},
+                             {return_raw = ret_raw}))
+    t.assert_equals(resf:tomap{names_only = true},
+                    tuple:tomap{names_only = true})
+    c.space.sf:replace{0}
+    res = res_wrapper(c.space.s:select({}, {return_raw = ret_raw}))[1]
+    t.assert_equals(res:totable(), tuple:totable())
+    resf = res_wrapper(c.space.sf:select({}, {return_raw = ret_raw}))[1]
+    t.assert_equals(resf:tomap{names_only = true},
+                    tuple:tomap{names_only = true})
+    c.space.s:insert{1}
+    c.space.sf:insert{1}
+
+    local tuples = {box.tuple.new({0}, {format = fmt}),
+                    box.tuple.new({1}, {format = fmt})}
+    res = res_wrapper(c.space.s:select({}, {return_raw = ret_raw}))
+    resf = res_wrapper(c.space.sf:select({}, {return_raw = ret_raw}))
+    for i, tpl in ipairs(tuples) do
+        t.assert_equals(res[i]:totable(), tpl:totable())
+        t.assert_equals(resf[i]:tomap{names_only = true},
+                        tpl:tomap{names_only = true})
+    end
+    t.assert_equals(pack(c:call("f")), {1, 2, 3, n = 3})
+    t.assert_equals(pack(c:eval("return 1, 2, 3")), {1, 2, 3, n = 3})
+
+    test_call_eval_box_tuple_extension(c, "call", tuple, ret_raw)
+    test_call_eval_box_tuple_extension(c, "eval", tuple, ret_raw)
+end
+
+-- Checks that `box_tuple_extension` backward compatibility option works
+-- correctly.
+g.test_box_tuple_extension_compat_option = function(cg)
+    cg.server:exec(function()
+        require('compat').box_tuple_extension = 'old'
+    end)
+
+    local c = netbox:connect(cg.server.net_box_uri, {fetch_schema = false})
+
+    t.assert_equals(pack(c:call("f")), {1, 2, 3, n = 3})
+    t.assert_equals(pack(c:eval("return 1, 2, 3")), {1, 2, 3, n = 3})
+
+    local tuple = box.tuple.new{0}
+    test_call_eval_old(c, "call", tuple)
+    test_call_eval_old(c, "eval", tuple)
+
+    local ibuf = buffer.ibuf()
+    local data_size = c:call("t", {}, {buffer = ibuf})
+    local res = msgpack.object_from_raw(ibuf.rpos, data_size):decode()
+    res = res[box.iproto.key.DATA][1][2].t
+    t.assert_equals(res, tuple:totable())
+end
+
+g.before_test('test_netbox_conn_with_disabled_dml_tuple_extension_errinj',
+              function()
+    box.error.injection.set('ERRINJ_NETBOX_FLIP_FEATURE',
+                            box.iproto.feature.dml_tuple_extension)
+end)
+
+-- Checks that net.box connection buffer argument works correctly with formats
+-- in IPROTO request responses.
+g.test_netbox_conn_with_disabled_dml_tuple_extension_errinj = function(cg)
+    t.tarantool.skip_if_not_debug()
+
+    local c = netbox:connect(cg.server.net_box_uri, {fetch_schema = false})
+    local res = c.space.sf:get{0}
+    t.assert_equals(res:tomap{names_only = true}, {})
+end
+
+g.after_test('test_netbox_conn_with_disabled_dml_tuple_extension_errinj',
+              function()
+    box.error.injection.set('ERRINJ_NETBOX_FLIP_FEATURE', -1)
+end)
diff --git a/test/box-py/iproto.result b/test/box-py/iproto.result
index d51578ac5b27c6307204bd7fb9427b1071e08673..3b9d4bf713b263d351f90e323c202a872e572ae7 100644
--- a/test/box-py/iproto.result
+++ b/test/box-py/iproto.result
@@ -210,11 +210,11 @@ Invalid MsgPack - request body
 # Invalid auth_type
 Invalid MsgPack - request body
 # Empty request body
-version=6, features=[0, 1, 2, 3, 4, 5, 6], auth_type=chap-sha1
+version=6, features=[0, 1, 2, 3, 4, 5, 6, 7, 8], auth_type=chap-sha1
 # Unknown version and features
-version=6, features=[0, 1, 2, 3, 4, 5, 6], auth_type=chap-sha1
+version=6, features=[0, 1, 2, 3, 4, 5, 6, 7, 8], auth_type=chap-sha1
 # Unknown request key
-version=6, features=[0, 1, 2, 3, 4, 5, 6], auth_type=chap-sha1
+version=6, features=[0, 1, 2, 3, 4, 5, 6, 7, 8], auth_type=chap-sha1
 
 #
 # gh-6257 Watchers
diff --git a/test/box-tap/gh-4954-merger-via-net-box.test.lua b/test/box-tap/gh-4954-merger-via-net-box.test.lua
index e2bd6f8b9f19f6dbd54c797aad46a35c621a8ab9..7d6e9a5937696d272cc7c0ae55a967177101bd91 100755
--- a/test/box-tap/gh-4954-merger-via-net-box.test.lua
+++ b/test/box-tap/gh-4954-merger-via-net-box.test.lua
@@ -86,7 +86,6 @@ box.schema.user.grant('guest', 'execute', 'universe', nil,
                       {if_not_exists = true})
 
 local test = tap.test('gh-4954-merger-via-net-box.test.lua')
-test:plan(6)
 
 local tuples = {
     {1},
@@ -94,17 +93,25 @@ local tuples = {
     {3},
 }
 
+test:plan(3 * #tuples + 3)
+
 local connection = net_box.connect(box.cfg.listen)
 
+local function check_res(test, res, test_name)
+    for i, t in ipairs(tuples) do
+        test:is_deeply(res[i]:totable(), t, test_name)
+    end
+end
+
 local res = connection:call('use_table_source', {tuples})
-test:is_deeply(res, tuples, 'verify table source')
+check_res(test, res, 'verify table source')
 local res = connection:call('use_buffer_source', {tuples})
-test:is_deeply(res, tuples, 'verify buffer source')
+check_res(test, res, 'verify buffer source')
 local res = connection:call('use_tuple_source', {tuples})
-test:is_deeply(res, tuples, 'verify tuple source')
+check_res(test, res, 'verify tuple source')
 
 local function test_verify_source_async(test, func_name, request_count)
-    test:plan(request_count)
+    test:plan(request_count * #tuples)
 
     local futures = {}
     for _ = 1, request_count do
@@ -113,7 +120,7 @@ local function test_verify_source_async(test, func_name, request_count)
     end
     for i = 1, request_count do
         local res = unpack(futures[i]:wait_result())
-        test:is_deeply(res, tuples, ('verify request %d'):format(i))
+        check_res(test, res, ('verify request %d'):format(i))
     end
 end
 
diff --git a/test/box/gh-4513-netbox-self-and-connect-interchangeable.result b/test/box/gh-4513-netbox-self-and-connect-interchangeable.result
index 2a4e64cd527c765c963707f5578011bdae4c6016..7369c29463a75e23496f973fef85cf1306a1ddd5 100644
--- a/test/box/gh-4513-netbox-self-and-connect-interchangeable.result
+++ b/test/box/gh-4513-netbox-self-and-connect-interchangeable.result
@@ -24,10 +24,6 @@ end
 --
 -- netbox:self and netbox:connect should work interchangeably
 --
-type(nb:eval('return box.tuple.new{1}')) -- table
- | ---
- | - table
- | ...
 type(nb:eval('return box.error.new(1, "test error")')) -- cdata
  | ---
  | - cdata
diff --git a/test/box/gh-4513-netbox-self-and-connect-interchangeable.test.lua b/test/box/gh-4513-netbox-self-and-connect-interchangeable.test.lua
index d05144c9e03f6108c5eb1eba198f724909f8e07c..9f04738a8497729e7ed572eb4c2bf1ec2fa0e560 100644
--- a/test/box/gh-4513-netbox-self-and-connect-interchangeable.test.lua
+++ b/test/box/gh-4513-netbox-self-and-connect-interchangeable.test.lua
@@ -13,7 +13,6 @@ end
 --
 -- netbox:self and netbox:connect should work interchangeably
 --
-type(nb:eval('return box.tuple.new{1}')) -- table
 type(nb:eval('return box.error.new(1, "test error")')) -- cdata
 type(nb:eval('return box.NULL')) -- cdata
 
diff --git a/test/box/net.box_iproto_id.result b/test/box/net.box_iproto_id.result
index d4ca898f44c53a02282aa93b3f688ec915c456bd..c2ce4d07ffb65068e2c2e2d2d08da1f2f229db3f 100644
--- a/test/box/net.box_iproto_id.result
+++ b/test/box/net.box_iproto_id.result
@@ -22,10 +22,12 @@ c.peer_protocol_features
  | - transactions: true
  |   watchers: true
  |   error_extension: true
- |   streams: true
  |   pagination: true
  |   space_and_index_names: true
+ |   streams: true
  |   watch_once: true
+ |   dml_tuple_extension: true
+ |   call_ret_tuple_extension: true
  | ...
 c:close()
  | ---
@@ -52,10 +54,12 @@ c.peer_protocol_features
  | - transactions: false
  |   watchers: false
  |   error_extension: false
- |   streams: false
  |   pagination: false
  |   space_and_index_names: false
+ |   streams: false
  |   watch_once: false
+ |   dml_tuple_extension: false
+ |   call_ret_tuple_extension: false
  | ...
 errinj.set('ERRINJ_IPROTO_DISABLE_ID', false)
  | ---
@@ -103,10 +107,12 @@ c.peer_protocol_features
  | - transactions: true
  |   watchers: true
  |   error_extension: true
- |   streams: true
  |   pagination: true
  |   space_and_index_names: true
+ |   streams: true
  |   watch_once: true
+ |   dml_tuple_extension: true
+ |   call_ret_tuple_extension: true
  | ...
 c:close()
  | ---
@@ -159,10 +165,12 @@ c.peer_protocol_features
  | - transactions: false
  |   watchers: true
  |   error_extension: true
- |   streams: true
  |   pagination: true
  |   space_and_index_names: true
+ |   streams: true
  |   watch_once: true
+ |   dml_tuple_extension: true
+ |   call_ret_tuple_extension: true
  | ...
 c:close()
  | ---
@@ -188,10 +196,12 @@ c.peer_protocol_features
  | - transactions: true
  |   watchers: true
  |   error_extension: true
- |   streams: true
  |   pagination: true
  |   space_and_index_names: true
+ |   streams: true
  |   watch_once: true
+ |   dml_tuple_extension: true
+ |   call_ret_tuple_extension: true
  | ...
 c:close()
  | ---
diff --git a/test/box/net.box_msgpack_gh-2195.result b/test/box/net.box_msgpack_gh-2195.result
index 665e1c25c8a7872acdb9122e2a5c87a109289ca3..c22fe56e4ff9e3f0e0b03b9feaab13b255ca8a8a 100644
--- a/test/box/net.box_msgpack_gh-2195.result
+++ b/test/box/net.box_msgpack_gh-2195.result
@@ -265,36 +265,36 @@ function echo(...) return ... end
 ...
 c:call("echo", {1, 2, 3}, {buffer = ibuf})
 ---
-- 10
+- 12
 ...
 result, ibuf.rpos = msgpack.decode_unchecked(ibuf.rpos)
 ---
 ...
 result
 ---
-- {48: [1, 2, 3]}
+- {96: {}, 48: [1, 2, 3]}
 ...
 c:call("echo", {}, {buffer = ibuf})
 ---
-- 7
+- 9
 ...
 result, ibuf.rpos = msgpack.decode_unchecked(ibuf.rpos)
 ---
 ...
 result
 ---
-- {48: []}
+- {96: {}, 48: []}
 ...
 c:call("echo", nil, {buffer = ibuf})
 ---
-- 7
+- 9
 ...
 result, ibuf.rpos = msgpack.decode_unchecked(ibuf.rpos)
 ---
 ...
 result
 ---
-- {48: []}
+- {96: {}, 48: []}
 ...
 -- call + skip_header
 c:call("echo", {1, 2, 3}, {buffer = ibuf, skip_header = true})
@@ -333,36 +333,36 @@ result
 -- eval
 c:eval("echo(...)", {1, 2, 3}, {buffer = ibuf})
 ---
-- 7
+- 9
 ...
 result, ibuf.rpos = msgpack.decode_unchecked(ibuf.rpos)
 ---
 ...
 result
 ---
-- {48: []}
+- {96: {}, 48: []}
 ...
 c:eval("echo(...)", {}, {buffer = ibuf})
 ---
-- 7
+- 9
 ...
 result, ibuf.rpos = msgpack.decode_unchecked(ibuf.rpos)
 ---
 ...
 result
 ---
-- {48: []}
+- {96: {}, 48: []}
 ...
 c:eval("echo(...)", nil, {buffer = ibuf})
 ---
-- 7
+- 9
 ...
 result, ibuf.rpos = msgpack.decode_unchecked(ibuf.rpos)
 ---
 ...
 result
 ---
-- {48: []}
+- {96: {}, 48: []}
 ...
 -- eval + skip_header
 c:eval("echo(...)", {1, 2, 3}, {buffer = ibuf, skip_header = true})
diff --git a/test/box/net.box_raw_response_gh-3107.result b/test/box/net.box_raw_response_gh-3107.result
index 1208341d572833910f4c6fabd56014ff63aefe07..7e9ffd53b79eca3fa215540e0bc82c0752aee3bf 100644
--- a/test/box/net.box_raw_response_gh-3107.result
+++ b/test/box/net.box_raw_response_gh-3107.result
@@ -103,14 +103,14 @@ finalize_long()
 ...
 future:wait_result(100)
 ---
-- 10
+- 12
 ...
 result, ibuf.rpos = msgpack.decode_unchecked(ibuf.rpos)
 ---
 ...
 result
 ---
-- {48: [1, 2, 3]}
+- {96: {}, 48: [1, 2, 3]}
 ...
 box.schema.func.drop('long_function')
 ---
diff --git a/test/box/push.result b/test/box/push.result
index e3e4d1253e4362fae87d4248ff1071e69e62ddb9..9654b78ab50cb7be343c4b471eb9f7cbaf0d8ddd 100644
--- a/test/box/push.result
+++ b/test/box/push.result
@@ -242,7 +242,7 @@ resp_len = c:call('do_pushes', {300}, {on_push = table.insert, on_push_ctx = mes
 ...
 resp_len
 ---
-- 10
+- 12
 ...
 messages
 ---
@@ -274,7 +274,7 @@ r, _ = msgpack.decode_unchecked(ibuf.rpos)
 ...
 r
 ---
-- {48: [300]}
+- {96: {}, 48: [300]}
 ...
 --
 -- Test error in __serialize.